"""Dayu (大鱼号) posting automation implementation (intermediate version).

This keeps code isolated from Xiaohongshu logic. You can iterate selectors
later without touching root modules. All selenium interactions are wrapped so
missing or changing elements won't crash the whole server; errors bubble as
messages you can inspect via job status.
"""
from __future__ import annotations

from dataclasses import dataclass
from typing import Any, List, Optional
import os
import sys
import time

try:
    from selenium import webdriver
    from selenium.webdriver.chrome.options import Options
    from selenium.webdriver.common.by import By
    from selenium.webdriver.support.ui import WebDriverWait
    from selenium.webdriver.support import expected_conditions as EC
    from selenium.common.exceptions import TimeoutException, WebDriverException
except Exception:  # pragma: no cover - selenium optional until installed
    webdriver = None  # type: ignore


def _bool(env_name: str, default: bool = False) -> bool:
    return os.getenv(env_name, str(default)).strip().lower() in {"1", "true", "yes", "on"}


def _env(name: str, default: str = "") -> str:
    return os.getenv(name, default)


@dataclass
class DayuSelectors:
    # 默认直接使用你提供的标题 id，仍允许通过环境变量覆盖
    title_input_css: str = _env("DAYU_SEL_TITLE", "#title")
    content_frame_css: str = _env("DAYU_SEL_CONTENT_FRAME", "iframe.editor-frame,iframe")
    # 常见富文本容器：优先 contenteditable，再覆盖常见编辑器
    content_editable_css: str = _env(
        "DAYU_SEL_CONTENT_EDITABLE",
        "div[contenteditable='true'],div.ql-editor[contenteditable='true'],div.ProseMirror,div.w-e-text"
    )
    upload_button_css: str = _env("DAYU_SEL_UPLOAD_BUTTON", "input[type='file']")
    # 提交按钮不再依赖无效的 :has-text CSS，使用 _click_submit 内部的 XPath 文案匹配
    submit_button_css: str = _env("DAYU_SEL_SUBMIT_BUTTON", "")


@dataclass
class DayuConfig:
    headless: bool = _bool("DAYU_HEADLESS", False)
    user_data_dir: Optional[str] = _env("DAYU_USER_DATA_DIR") or None
    # Changed default to dashboard index per user provided URL
    publish_url: str = _env("DAYU_PUBLISH_URL", "https://mp.dayu.com/dashboard/index")
    wait_timeout: int = int(_env("DAYU_WAIT_TIMEOUT", "40"))
    keep_browser_open: bool = _bool("DAYU_KEEP_BROWSER_OPEN", False)
    slow_mode: bool = _bool("DAYU_SLOW_MODE", False)
    debug_dump: bool = _bool("DAYU_DEBUG_DUMP", False)
    screenshot_dir: Optional[str] = _env("DAYU_SCREENSHOT_DIR") or None
    auto_detect: bool = _bool("DAYU_AUTO_DETECT", True)
    js_force_fill: bool = _bool("DAYU_JS_FORCE_FILL", True)
    # New options for author/cover/source
    # 写死默认作者与封面/来源策略（仍可通过环境变量覆盖）
    default_author_name: Optional[str] = _env("DAYU_AUTHOR_NAME") or "启明云端"
    cover_single: bool = _bool("DAYU_COVER_SINGLE", True)
    cover_from_content: bool = _bool("DAYU_COVER_FROM_CONTENT", True)
    cover_vertical: bool = _bool("DAYU_COVER_VERTICAL", True)
    info_source: Optional[str] = _env("DAYU_INFO_SOURCE") or "无需标注"


class DayuPoster:
    """Automates Dayu (大鱼号) article creation.

    Notes:
    - This class purposefully does not persist cookies yet; rely on Chrome user data dir.
    - Adjust selectors using environment overrides without code changes.
    - All interactions guarded with timeouts to avoid hanging threads.
    """
    def __init__(self, config: DayuConfig | None = None, selectors: DayuSelectors | None = None, account: str | None = None):
        self.config = config or DayuConfig()
        self.selectors = selectors or DayuSelectors()
        self.account = account
        self.driver = None
        
        # 根据 account 参数调整 Chrome Profile 目录（多账号支持）
        if self.account and self.config.user_data_dir:
            # 为指定账号创建专用的 profile 目录: {base_dir}_{account}
            base_dir = self.config.user_data_dir.rstrip('/\\')
            self.profile_dir = f"{base_dir}_{self.account}"
        else:
            # 使用原始的 profile 目录
            self.profile_dir = self.config.user_data_dir
        # simple in-memory click counters for diagnostics
        self._click_counts = {}
        self._last_single_cover_debug: dict[str, Any] | None = None
        # heuristic markers to judge login state
        self.login_marker_selectors = [
            os.getenv("DAYU_SEL_LOGIN_BUTTON", "button:contains('登录'),button:contains('登入')"),
            "button.login-btn",
            "a.login-btn",
        ]
        self.post_login_indicators = [
            os.getenv("DAYU_SEL_AVATAR", "img.avatar,div.avatar"),
            "a[href*='logout']",
        ]

    # ---------------- Driver Management -----------------
    def _build_driver(self):
        if webdriver is None:
            raise RuntimeError("selenium not available - install selenium & a matching chromedriver")
        options = Options()
        if self.config.headless:
            options.add_argument("--headless=new")
        options.add_argument("--disable-gpu")
        options.add_argument("--no-sandbox")
        options.add_argument("--disable-dev-shm-usage")
        # 使用计算后的 profile_dir（支持多账号）
        if self.profile_dir:
            options.add_argument(f"--user-data-dir={self.profile_dir}")
        options.add_argument("--window-size=1280,900")
        driver = webdriver.Chrome(options=options)
        return driver

    def ensure_driver(self):
        if self.driver is None:
            self.driver = self._build_driver()
        return self.driver

    # ---------------- Login Handling -----------------
    def _is_logged_in(self) -> bool:
        d = self.ensure_driver()
        try:
            # If avatar or logout link present -> logged in
            for css in self.post_login_indicators:
                if not css:
                    continue
                try:
                    found = d.find_elements(By.CSS_SELECTOR, css)
                    if found:
                        return True
                except Exception:
                    continue
            return False
        except Exception:
            return False

    def ensure_logged_in(self, max_wait: int | None = None):
        """Ensure user is logged in; if not, wait for manual login.

        Strategy:
        1. Check quick markers (avatar/logout)
        2. If not logged in, wait up to max_wait (default 180s) for state to change.
        3. During wait, poll every 2s; user can manually complete login (QR, password, etc.).
        4. Returns True if logged in else False.
        """
        d = self.ensure_driver()
        if self._is_logged_in():
            return True
        # we are probably on login page now
        end_time = time.time() + (max_wait or int(os.getenv("DAYU_LOGIN_MAX_WAIT", "180")))
        while time.time() < end_time:
            if self._is_logged_in():
                return True
            time.sleep(2)
        return self._is_logged_in()

    # ---------------- Element Helpers -----------------
    def _find(self, selector: str):
        """支持 CSS 或 XPath（以 // 或 xpath: 开头）"""
        d = self.ensure_driver()
        try:
            if not selector:
                return None
            if selector.startswith("xpath:"):
                return d.find_element(By.XPATH, selector[len("xpath:"):])
            if selector.startswith("//") or selector.startswith("(//"):
                return d.find_element(By.XPATH, selector)
            return d.find_element(By.CSS_SELECTOR, selector)
        except Exception:
            return None

    def _find_any(self, *selectors: str):
        for sel in selectors:
            el = self._find(sel)
            if el is not None:
                return el
        return None

    # ---------------- Navigation -----------------
    def _enter_editor(self, timeout: int | None = None):
        """从仪表盘进入写作页：点击“发布内容”，必要时点击“图文/文章”，等待标题框出现。"""
        d = self.ensure_driver()
        t = timeout or self.config.wait_timeout
        clicked = False
        # 1) 点击“发布内容”
        for xp in [
            "//span[contains(text(),'发布内容')]/ancestor::a|//span[contains(text(),'发布内容')]/ancestor::button",
            "//span[contains(text(),'发布内容')]",
            "//a[contains(.,'发布内容')]",
        ]:
            try:
                node = d.find_element(By.XPATH, xp)
                d.execute_script("arguments[0].scrollIntoView({block:'center'});", node)
                try:
                    node.click()
                except Exception:
                    d.execute_script("arguments[0].click();", node)
                clicked = True
                time.sleep(1.0 if self.config.slow_mode else 0.5)
                break
            except Exception:
                continue
        # 2) 某些站点需再选“图文/文章/发布图文”
        if clicked:
            for xp in [
                "//span[contains(text(),'图文')]/ancestor::a|//span[contains(text(),'图文')]/ancestor::button|//span[contains(text(),'图文')]",
                "//span[contains(text(),'文章')]/ancestor::a|//span[contains(text(),'文章')]/ancestor::button|//span[contains(text(),'文章')]",
                "//span[contains(text(),'发布图文')]/ancestor::a|//span[contains(text(),'发布图文')]/ancestor::button|//span[contains(text(),'发布图文')]",
            ]:
                try:
                    tab = d.find_element(By.XPATH, xp)
                    d.execute_script("arguments[0].scrollIntoView({block:'center'});", tab)
                    try:
                        tab.click()
                    except Exception:
                        d.execute_script("arguments[0].click();", tab)
                    time.sleep(0.8 if self.config.slow_mode else 0.4)
                    break
                except Exception:
                    continue
        # 3) 等待标题输入出现
        try:
            WebDriverWait(d, t).until(EC.presence_of_element_located((By.CSS_SELECTOR, self.selectors.title_input_css)))
        except Exception:
            pass

    # ---------------- Input Helpers -----------------
    def _safe_send_keys(self, el, text: str) -> bool:
        try:
            try:
                el.clear()
            except Exception:
                pass
            try:
                el.click()
            except Exception:
                pass
            el.send_keys(text)
            return True
        except Exception:
            return False

    def _js_force_input(self, el, text: str) -> bool:
        d = self.ensure_driver()
        try:
            d.execute_script(
                """
                const el = arguments[0];
                const val = arguments[1];
                try { el.focus(); } catch(e) {}
                if ('value' in el) { el.value = val; }
                else { el.textContent = val; }
                try { el.dispatchEvent(new Event('input', { bubbles: true })); } catch(e) {}
                try { el.dispatchEvent(new Event('change', { bubbles: true })); } catch(e) {}
                """,
                el,
                text,
            )
            return True
        except Exception:
            return False

    def _js_force_contenteditable(self, el, html: str) -> bool:
        d = self.ensure_driver()
        try:
            # 直接设置 innerHTML，并触发输入事件
            d.execute_script(
                """
                const el = arguments[0];
                const val = arguments[1];
                try { el.focus(); } catch(e) {}
                if (el.getAttribute('contenteditable') === 'true') {
                    el.innerHTML = val.replace(/\n/g, '<br>');
                } else {
                    el.textContent = val;
                }
                try { el.dispatchEvent(new Event('input', { bubbles: true })); } catch(e) {}
                try { el.dispatchEvent(new Event('change', { bubbles: true })); } catch(e) {}
                """,
                el,
                html,
            )
            return True
        except Exception:
            return False

    def _upload_images(self, images):
        if not images:
            return 0
        d = self.ensure_driver()
        upload_meta = {
            "attempted_open_uploader": False,
            "top_inputs_count": 0,
            "found_any_input": False,
            "found_any_input_iframe_index": None,
            "found_any_input_used_shadow": False,
            "used_top_label_mapping": False,
            "used_iframe_label_mapping": False,
        }
        # 过滤有效本地文件路径
        valid_paths = []
        try:
            for p in images:
                if isinstance(p, str) and os.path.exists(p):
                    valid_paths.append(p)
        except Exception:
            pass
        if not valid_paths:
            return 0
        sent = 0
        # 尝试先聚焦正文区域，保证上传后更可能插入到正文
        try:
            el, frame = self._find_content_element()
            if el:
                try:
                    if frame is not None:
                        d.switch_to.frame(frame)
                    d.execute_script("arguments[0].scrollIntoView({block:'center'});", el)
                    try:
                        el.click()
                    except Exception:
                        d.execute_script("arguments[0].focus();", el)
                finally:
                    try:
                        d.switch_to.default_content()
                    except Exception:
                        pass
        except Exception:
            pass
        try:
            # 1. 自动点击插入图片按钮（wm-icon-pic），确保弹出上传区域
            try:
                pic_btns = d.find_elements(By.CSS_SELECTOR, ".wm-icon-pic, .edui-icon.iconfont.wm-icon-pic")
                for btn in pic_btns:
                    try:
                        btn.click()
                        time.sleep(0.5)
                        break
                    except Exception:
                        pass
            except Exception:
                pass
            # 2. 查找.upload-input并send_keys本地图片路径
            try:
                upload_inputs = d.find_elements(By.CSS_SELECTOR, ".upload-input")
            except Exception:
                upload_inputs = []
            if upload_inputs:
                for inp in upload_inputs:
                    try:
                        inp.send_keys("\n".join(valid_paths))
                        sent = len(valid_paths)
                        time.sleep(10)  # 上传图片后等待10秒
                        # 上传完成后点击确认按钮
                        try:
                            confirm_btns = d.find_elements(By.CSS_SELECTOR, ".w-btn.w-btn_primary")
                            for btn in confirm_btns:
                                try:
                                    btn.click()
                                    # metrics
                                    try:
                                        self._click_counts['upload_confirm'] = self._click_counts.get('upload_confirm', 0) + 1
                                    except Exception:
                                        pass
                                    time.sleep(10)  # 点击确认后等待10秒
                                    break
                                except Exception:
                                    pass
                        except Exception:
                            pass
                        break
                    except Exception:
                        pass
            # 3. 如果.upload-input失败，兜底查找input[type='file']
            if not sent:
                try:
                    inputs = d.find_elements(By.CSS_SELECTOR, "input[type='file']")
                except Exception:
                    inputs = []
                for inp in inputs:
                    try:
                        inp.send_keys("\n".join(valid_paths))
                        sent = len(valid_paths)
                        time.sleep(10)  # 上传图片后等待10秒
                        # 点击确认按钮
                        try:
                            confirm_btns = d.find_elements(By.CSS_SELECTOR, ".w-btn.w-btn_primary")
                            for btn in confirm_btns:
                                try:
                                    btn.click()
                                    try:
                                        self._click_counts['upload_confirm'] = self._click_counts.get('upload_confirm', 0) + 1
                                    except Exception:
                                        pass
                                    time.sleep(10)
                                    break
                                except Exception:
                                    pass
                        except Exception:
                            pass
                        break
                    except Exception:
                        pass
            if not sent:
                frames = d.find_elements(By.TAG_NAME, "iframe")
                for f in frames:
                    try:
                        d.switch_to.frame(f)
                        # 1) 直接找 input
                        frame_inputs = []
                        try:
                            frame_inputs = d.find_elements(By.CSS_SELECTOR, "input[type='file']")
                        except Exception:
                            frame_inputs = []
                        # 2) 找 label 关联的 input
                        labels = []
                        try:
                            labels = d.find_elements(By.TAG_NAME, "label")
                        except Exception:
                            labels = []
                        # 优先 input
                        for inp in frame_inputs:
                            try:
                                try:
                                    self._reveal_file_input(inp)
                                except Exception:
                                    pass
                                inp.send_keys("\n".join(valid_paths))
                                sent = len(valid_paths)
                                time.sleep(10)  # 上传图片后等待10秒
                                try:
                                    confirm_btns = d.find_elements(By.CSS_SELECTOR, ".w-btn.w-btn_primary")
                                    for btn in confirm_btns:
                                        try:
                                            btn.click()
                                            try:
                                                self._click_counts['upload_confirm'] = self._click_counts.get('upload_confirm', 0) + 1
                                            except Exception:
                                                pass
                                            time.sleep(10)
                                            break
                                        except Exception:
                                            pass
                                except Exception:
                                    pass
                                break
                            except Exception:
                                pass
                        # 再尝试通过 label 反查 input
                        if not sent:
                            for lab in labels:
                                try:
                                    for_attr = lab.get_attribute('for')
                                except Exception:
                                    for_attr = None
                                target_inp = None
                                if for_attr:
                                    try:
                                        target_inp = d.find_element(By.ID, for_attr)
                                    except Exception:
                                        target_inp = None
                                if not target_inp:
                                    # 使用相邻/后代查找
                                    try:
                                        target_inp = d.find_element(By.XPATH, ".//following::input[@type='file'][1]")
                                    except Exception:
                                        target_inp = None
                                if target_inp:
                                    try:
                                        try:
                                            self._reveal_file_input(target_inp)
                                        except Exception:
                                            pass
                                        target_inp.send_keys("\n".join(valid_paths))
                                        upload_meta["used_iframe_label_mapping"] = True
                                        sent = len(valid_paths)
                                        time.sleep(10)  # 上传图片后等待10秒
                                        try:
                                            confirm_btns = d.find_elements(By.CSS_SELECTOR, ".w-btn.w-btn_primary")
                                            for btn in confirm_btns:
                                                try:
                                                    btn.click()
                                                    try:
                                                        self._click_counts['upload_confirm'] = self._click_counts.get('upload_confirm', 0) + 1
                                                    except Exception:
                                                        pass
                                                    time.sleep(10)
                                                    break
                                                except Exception:
                                                    pass
                                        except Exception:
                                            pass
                                        break
                                    except Exception:
                                        pass
                            if sent:
                                break
                    except Exception:
                        pass
                    finally:
                        try:
                            d.switch_to.default_content()
                        except Exception:
                            pass
        except Exception:
            pass
        # 发送后，等待正文中出现 <img>（最多 5 秒，简易等待）
        if sent:
            deadline = time.time() + 5
            while time.time() < deadline:
                try:
                    if self._count_images_in_content() >= 1:
                        break
                except Exception:
                    pass
                time.sleep(0.2)
        # 保存调试元数据
        try:
            self._last_upload_debug = {**upload_meta, "sent_files": sent}
        except Exception:
            self._last_upload_debug = {"sent_files": sent}
        return sent

    def _count_images_in_content(self) -> int:
        """统计正文区域内的 <img> 数量（考虑 iframe）。"""
        d = self.ensure_driver()
        count = 0
        try:
            el, frame = self._find_content_element()
            if not el:
                return 0
            if frame is not None:
                d.switch_to.frame(frame)
            try:
                count = len(d.find_elements(By.CSS_SELECTOR, "img"))
            finally:
                try:
                    d.switch_to.default_content()
                except Exception:
                    pass
        except Exception:
            count = 0
        return count

    def _reveal_file_input(self, el) -> bool:
        d = self.ensure_driver()
        try:
            d.execute_script(
                """
                const el = arguments[0];
                try { el.removeAttribute('hidden'); } catch(e) {}
                try { el.disabled = false; } catch(e) {}
                try {
                  el.style.cssText = 'display:block !important; visibility:visible !important; opacity:1 !important; '
                    + 'position:fixed !important; left:20px !important; top:20px !important; z-index:2147483647 !important; '
                    + 'width:2px !important; height:2px !important; pointer-events:auto !important;';
                } catch(e) {}
                return true;
                """,
                el,
            )
            return True
        except Exception:
            return False

    def _find_file_input_anywhere(self):
        """寻找文件选择 input（含 shadow DOM 与各 iframe）。返回 (el, iframe_index, used_shadow)。"""
        d = self.ensure_driver()
        # 顶层：先直接找，再找 shadow
        try:
            els = d.find_elements(By.CSS_SELECTOR, "input[type='file']")
            if els:
                return els[0], None, False
        except Exception:
            pass
        try:
            el = self._find_including_shadow("input[type='file']")
            if el:
                return el, None, True
        except Exception:
            pass
        # iframes
        frames = d.find_elements(By.TAG_NAME, "iframe")
        for idx, f in enumerate(frames):
            try:
                d.switch_to.frame(f)
                try:
                    els = d.find_elements(By.CSS_SELECTOR, "input[type='file']")
                    if els:
                        return els[0], idx, False
                except Exception:
                    pass
                try:
                    el = self._find_including_shadow("input[type='file']")
                    if el:
                        return el, idx, True
                except Exception:
                    pass
            except Exception:
                pass
            finally:
                try:
                    d.switch_to.default_content()
                except Exception:
                    pass
        return None, None, False

    # ---------------- High-level Flow -----------------
    def open_publish_page(self):
        d = self.ensure_driver()
        d.get(self.config.publish_url)
        # 登录与初步等待
        self.ensure_logged_in()
        time.sleep(2 if self.config.slow_mode else 0.8)
        # 从仪表盘进入编辑页
        try:
            current_url = d.current_url
            if "dashboard" in current_url:
                self._enter_editor()
        except Exception:
            pass

    # ---------------- Core Actions -----------------
    def _fill_title(self, title: str):
        # 优先按 #title，失败走自动探测与 JS 兜底
        d = self.ensure_driver()
        el = self._find_any(
            self.selectors.title_input_css,
            "#title",
            "input#title",
            "input.article-write_box-title-input",
        )
        if el:
            if self._safe_send_keys(el, title) or self._js_force_input(el, title):
                return True
        # 自动探测 fallback
        auto_el = self._auto_detect_title()
        if auto_el and (self._safe_send_keys(auto_el, title) or self._js_force_input(auto_el, title)):
            return True
        # 直接 ID 最终尝试
        try:
            el2 = d.find_element(By.ID, "title")
            if self._safe_send_keys(el2, title) or self._js_force_input(el2, title):
                return True
        except Exception:
            pass
        return False

    def _find_content_element(self):
        """在顶层与各 iframe 内查找正文可编辑区域，返回(元素, 所在iframe或None)。"""
        d = self.ensure_driver()
        # 记录统计信息
        self._iframe_count = 0
        self._content_iframe_index = None
        self._content_used_shadow = False
        content_selectors = [
            self.selectors.content_editable_css,
            "div.ql-editor[contenteditable='true']",
            "div[contenteditable='true']",
            "div.ProseMirror",
            "div.w-e-text",
            "section[contenteditable='true']",
            "article[contenteditable='true']",
            "body[contenteditable='true']",
            "textarea#content",
        ]
        # 顶层先找
        for css in content_selectors:
            el = self._find(css)
            if el:
                return el, None
            # shadow DOM 深度查找
            el_shadow = self._find_including_shadow(css)
            if el_shadow:
                self._content_used_shadow = True
                return el_shadow, None
        # iframe 中找
        frames = d.find_elements(By.TAG_NAME, "iframe")
        try:
            self._iframe_count = len(frames)
        except Exception:
            self._iframe_count = 0
        for idx, f in enumerate(frames):
            try:
                d.switch_to.frame(f)
                for css in content_selectors:
                    el = self._find(css)
                    if el:
                        self._content_iframe_index = idx
                        return el, f
                    el_shadow = self._find_including_shadow(css)
                    if el_shadow:
                        self._content_iframe_index = idx
                        self._content_used_shadow = True
                        return el_shadow, f
            except Exception:
                pass
            finally:
                try:
                    d.switch_to.default_content()
                except Exception:
                    pass
        return None, None

    def _find_including_shadow(self, css_selector: str):
        """在 shadow DOM 中递归搜索匹配的元素，返回第一个匹配的元素或 None。"""
        d = self.ensure_driver()
        if not css_selector:
            return None
        js = r"""
            const sel = arguments[0];
            function* walk(node){
                yield node;
                const children = node.children || [];
                for (const c of children){
                    yield* walk(c);
                }
                if (node.shadowRoot){
                    const sr = node.shadowRoot;
                    // 先在当前 shadow root 内直接匹配
                    const found = sr.querySelector(sel);
                    if (found) return found; // 提前短路返回元素本身
                    // 否则继续递归 shadow root 内部
                    const stack = [sr];
                    while (stack.length){
                        const cur = stack.pop();
                        for (const kid of cur.children || []){
                            // 直接匹配
                            const hit = kid.matches ? kid.matches(sel) : false;
                            if (hit) return kid;
                            if (kid.shadowRoot){
                                // 深度优先
                                const innerFound = kid.shadowRoot.querySelector(sel);
                                if (innerFound) return innerFound;
                                stack.push(kid.shadowRoot);
                            }
                            // 继续常规孩子
                            for (const g of kid.children || []){
                                stack.push(g);
                            }
                        }
                    }
                }
            }
            // 顶层文档先试直接 querySelector
            const direct = document.querySelector(sel);
            if (direct) return direct;
            // 否则遍历整个文档树并穿透所有 shadow roots
            const treeWalker = document.documentElement || document.body || document;
            const it = walk(treeWalker);
            let n = it.next();
            while(!n.done){
                const node = n.value;
                try{
                    if (node && node.matches && node.matches(sel)) return node;
                }catch(e){}
                n = it.next();
            }
            return null;
        """
        try:
            el = d.execute_script(js, css_selector)
            return el or None
        except Exception:
            return None

    def _dispatch_click_sequence(self, el) -> bool:
        """对元素派发一系列 pointer/mouse 事件，兼容仅响应 ::before 的场景。"""
        if el is None:
            return False
        d = self.ensure_driver()
        try:
            d.execute_script(
                """
                const el = arguments[0];
                if (!el) return false;
                const rect = el.getBoundingClientRect();
                const x = rect.left + rect.width / 2;
                const y = rect.top + rect.height / 2;
                const pointerOpts = {bubbles:true, clientX:x, clientY:y, pointerId:1, pointerType:'mouse'};
                const mouseOpts = {bubbles:true, clientX:x, clientY:y};
                try { el.focus({preventScroll:true}); } catch(e){}
                const pointerDownEvents = ['pointerenter','pointerover','pointerdown'];
                for (const type of pointerDownEvents) {
                    try { el.dispatchEvent(new PointerEvent(type, pointerOpts)); } catch(e){}
                }
                try { el.dispatchEvent(new MouseEvent('mousedown', mouseOpts)); } catch(e){}
                const pointerUpEvents = ['pointerup','pointerout','pointerleave'];
                for (const type of pointerUpEvents) {
                    try { el.dispatchEvent(new PointerEvent(type, pointerOpts)); } catch(e){}
                }
                try { el.dispatchEvent(new MouseEvent('mouseup', mouseOpts)); } catch(e){}
                try { el.dispatchEvent(new MouseEvent('click', mouseOpts)); } catch(e){}
                return true;
                """,
                el,
            )
            return True
        except Exception:
            try:
                d.execute_script("arguments[0].click();", el)
                return True
            except Exception:
                try:
                    el.click()
                    return True
                except Exception:
                    return False

    def _fill_content(self, content: str):
        d = self.ensure_driver()
        el, frame = self._find_content_element()
        ok = False
        if el:
            try:
                if frame is not None:
                    d.switch_to.frame(frame)
                ok = self._safe_send_keys(el, content)
                if not ok:
                    ok = self._js_force_contenteditable(el, content) or self._js_force_input(el, content)
            except Exception:
                ok = False
            finally:
                try:
                    d.switch_to.default_content()
                except Exception:
                    pass
        return ok

    def _click_submit(self):
        d = self.ensure_driver()
        # 先定位一个“编辑器容器”：以标题输入或正文容器的最近父级 section/div 为准
        editor_container = None
        try:
            title_el = self._find_any(self.selectors.title_input_css, "#title", "input#title")
        except Exception:
            title_el = None
        if title_el is not None:
            try:
                editor_container = d.execute_script("return arguments[0].closest('section,div,main,form,article');", title_el)
            except Exception:
                editor_container = None
        if editor_container is None:
            try:
                content_el, frame = self._find_content_element()
                if content_el is not None:
                    if frame is not None:
                        d.switch_to.frame(frame)
                    try:
                        editor_container = d.execute_script("return arguments[0].closest('section,div,main,form,article');", content_el)
                    finally:
                        try:
                            d.switch_to.default_content()
                        except Exception:
                            pass
            except Exception:
                editor_container = None

        # 优先使用 XPath 文案匹配（发布/提交/发表），并带 JS 兜底点击
        submit_xpaths = [
            # 仅限 button，避免误点侧栏 a 链接
            "//button[.//span[contains(.,'发布')] or contains(.,'发布')]",
            "//button[.//span[contains(.,'提交')] or contains(.,'提交')]",
            "//button[.//span[contains(.,'发表')] or contains(.,'发表')]",
        ]
        # 优先匹配你提供的 DOM 特征
        submit_css_first = [
            # 最精确匹配：同时匹配类名和两个data属性
            "button.w-btn.w-btn_primary[data-spm-click*='dayuwebsite.articlewrite.publish'][data-spm-anchor-id*='d-submit']",
            "button.w-btn.w-btn_primary[data-spm-click*='articlewrite.publish']",
            "button.w-btn.w-btn_primary[data-spm-click*='locaid=d-submit']",
            "button[data-spm-anchor-id*='d-submit']",
            "button[data-spm-click*='articlewrite.publish']",
        ]
        candidates = []
        candidates.extend(submit_css_first)
        if self.selectors.submit_button_css:
            candidates.append(self.selectors.submit_button_css)
        candidates.extend([f"xpath:{xp}" for xp in submit_xpaths])
        # 第一阶段：若找到编辑器容器，优先在容器内查找 submit 按钮
        if editor_container is not None:
            for sel in candidates:
                try:
                    if sel.startswith("xpath:"):
                        btn = editor_container.find_element(By.XPATH, sel[len("xpath:"):])
                    elif sel.startswith("//") or sel.startswith("(//"):
                        btn = editor_container.find_element(By.XPATH, sel)
                    else:
                        btn = editor_container.find_element(By.CSS_SELECTOR, sel)
                except Exception:
                    btn = None
                if not btn:
                    continue
                # 禁用态过滤
                try:
                    aria_dis = (btn.get_attribute('aria-disabled') or '').lower() in ['true', '1']
                    dis_attr = btn.get_attribute('disabled') is not None
                    class_dis = 'disabled' in (btn.get_attribute('class') or '')
                    if aria_dis or dis_attr or class_dis:
                        continue
                except Exception:
                    pass
                try:
                    d.execute_script("arguments[0].scrollIntoView({block:'center'});", btn)
                except Exception:
                    pass
                # 等待可点击（最多 3s）
                t0 = time.time()
                while time.time() - t0 < 3:
                    try:
                        if btn.is_enabled() and btn.is_displayed():
                            break
                    except Exception:
                        break
                    time.sleep(0.1)
                try:
                    btn.click()
                    try:
                        self._click_counts['submit'] = self._click_counts.get('submit', 0) + 1
                    except Exception:
                        pass
                    # 点击发表按钮成功后，处理确认发表弹窗
                    return self._click_confirm_publish()
                except Exception:
                    try:
                        d.execute_script("arguments[0].click();", btn)
                        try:
                            self._click_counts['submit'] = self._click_counts.get('submit', 0) + 1
                        except Exception:
                            pass
                        # 点击发表按钮成功后，处理确认发表弹窗
                        return self._click_confirm_publish()
                    except Exception:
                        continue

        # 第二阶段：全局查找，但继续过滤导航区域，并排除跳转到 dashboard 的 a 链接
        for sel in candidates:
            btn = self._find(sel)
            if not btn:
                continue
            # 过滤导航/侧边容器中的按钮
            try:
                in_nav = d.execute_script('return !!arguments[0].closest("aside,nav,header,[class*=\'menu\'],[class*=\'nav\'],[class*=\'sider\'],[class*=\'sidebar\']");', btn)
                if in_nav:
                    continue
            except Exception:
                pass
            # 排除明显导航/跳转链接
            try:
                tag = (btn.tag_name or '').lower()
                if tag == 'a':
                    href = (btn.get_attribute('href') or '').lower()
                    if any(x in href for x in ['dashboard', 'home', 'index', 'manage']):
                        continue
            except Exception:
                pass
            # 过滤禁用态
            try:
                aria_dis = (btn.get_attribute('aria-disabled') or '').lower() in ['true', '1']
                dis_attr = btn.get_attribute('disabled') is not None
                class_dis = 'disabled' in (btn.get_attribute('class') or '')
                if aria_dis or dis_attr or class_dis:
                    continue
            except Exception:
                pass
            try:
                d.execute_script("arguments[0].scrollIntoView({block:'center'});", btn)
            except Exception:
                pass
            try:
                if btn.is_enabled() and btn.is_displayed():
                    btn.click()
                    try:
                        self._click_counts['submit'] = self._click_counts.get('submit', 0) + 1
                    except Exception:
                        pass
                    # 点击发表按钮成功后，处理确认发表弹窗
                    return self._click_confirm_publish()
            except Exception:
                try:
                    d.execute_script("arguments[0].click();", btn)
                    try:
                        self._click_counts['submit'] = self._click_counts.get('submit', 0) + 1
                    except Exception:
                        pass
                    # 点击发表按钮成功后，处理确认发表弹窗
                    return self._click_confirm_publish()
                except Exception:
                    continue
        return False

    def _click_confirm_publish(self) -> bool:
        """处理预览弹窗中的"确认发表"按钮点击"""
        d = self.ensure_driver()
        
        # 等待预览弹窗出现
        time.sleep(2)
        
        # 处理可能出现的新窗口
        try:
            # 检查是否有新窗口
            original_window = d.current_window_handle
            all_windows = d.window_handles
            
            if len(all_windows) > 1:
                # 切换到新窗口（预览窗口）
                for window in all_windows:
                    if window != original_window:
                        d.switch_to.window(window)
                        break
        except Exception:
            pass
        
        # 查找"确认发表"按钮的选择器
        confirm_selectors = [
            # 用户提供的精确选择器
            "button.w-btn.w-btn_primary[data-spm-anchor-id*='i5']",
            "button.w-btn.w-btn_primary[data-spm-anchor-id*='2aqu7b']",
            # 通用选择器
            "button.w-btn.w-btn_primary",
            # XPath 文案匹配
            "xpath://button[contains(text(), '确认发表')]",
            "xpath://button[.//span[contains(text(), '确认发表')]]",
            "xpath://button[contains(., '确认发表')]",
        ]
        
        # 尝试点击确认发表按钮
        for selector in confirm_selectors:
            try:
                if selector.startswith("xpath:"):
                    btn = d.find_element(By.XPATH, selector[len("xpath:"):])
                else:
                    btn = d.find_element(By.CSS_SELECTOR, selector)
                
                if btn and btn.is_displayed() and btn.is_enabled():
                    # 滚动到按钮位置
                    try:
                        d.execute_script("arguments[0].scrollIntoView({block:'center'});", btn)
                    except Exception:
                        pass
                    
                    # 点击按钮
                    try:
                        btn.click()
                        print("✓ 成功点击确认发表按钮")
                        try:
                            self._click_counts['confirm_publish'] = self._click_counts.get('confirm_publish', 0) + 1
                        except Exception:
                            pass
                        
                        # 等待发表完成
                        time.sleep(3)
                        return True
                    except Exception:
                        try:
                            d.execute_script("arguments[0].click();", btn)
                            print("✓ 成功点击确认发表按钮（JS方式）")
                            try:
                                self._click_counts['confirm_publish'] = self._click_counts.get('confirm_publish', 0) + 1
                            except Exception:
                                pass
                            time.sleep(3)
                            return True
                        except Exception:
                            continue
            except Exception:
                continue
        
        print("⚠️ 未找到确认发表按钮")
        return False

    def _coalesce_bool(self, override: Optional[bool], default: bool) -> bool:
        return default if override is None else bool(override)

    def _click_in_any_context(self, selectors: list[str]) -> bool:
        """在顶层与各 iframe 内尝试点击任一 selector（支持 CSS/xpath: 前缀），成功返回 True。"""
        d = self.ensure_driver()
        # 顶层
        for sel in selectors:
            el = self._find(sel)
            if el:
                try:
                    d.execute_script("arguments[0].scrollIntoView({block:'center'});", el)
                except Exception:
                    pass
                try:
                    el.click()
                    return True
                except Exception:
                    try:
                        d.execute_script("arguments[0].click();", el)
                        return True
                    except Exception:
                        pass
        # iframes
        frames = d.find_elements(By.TAG_NAME, "iframe")
        for f in frames:
            try:
                d.switch_to.frame(f)
                for sel in selectors:
                    try:
                        if sel.startswith("xpath:"):
                            el = d.find_element(By.XPATH, sel[len("xpath:"):])
                        elif sel.startswith("//") or sel.startswith("(//"):
                            el = d.find_element(By.XPATH, sel)
                        else:
                            el = d.find_element(By.CSS_SELECTOR, sel)
                    except Exception:
                        el = None
                    if el:
                        try:
                            d.execute_script("arguments[0].scrollIntoView({block:'center'});", el)
                        except Exception:
                            pass
                        try:
                            el.click(); d.switch_to.default_content(); return True
                        except Exception:
                            try:
                                d.execute_script("arguments[0].click();", el); d.switch_to.default_content(); return True
                            except Exception:
                                pass
            except Exception:
                pass
            finally:
                try:
                    d.switch_to.default_content()
                except Exception:
                    pass
        return False

    def _open_editor_image_uploader(self) -> bool:
        """点击编辑器工具栏的插图按钮（wm-icon-pic/edui-icon等），尽量唤起文件上传控件。"""
        d = self.ensure_driver()
        selectors = [
            ".wm-icon-pic",
            ".edui-icon-pic",
            ".edui-icon.iconfont.wm-icon-pic",
            "xpath://*[contains(@class,'wm-icon-pic')]",
            "xpath://*[contains(@class,'edui-icon') and (contains(@class,'pic') or contains(.,'图片'))]",
            "xpath://*[contains(text(),'插入图片') or contains(text(),'图片') or contains(@aria-label,'图片')]",
        ]
        clicked = self._click_in_any_context(selectors)
        if clicked:
            time.sleep(0.4 if self.config.slow_mode else 0.2)
        # 判定：是否出现文件 input 或覆盖 label
        try:
            if d.find_elements(By.CSS_SELECTOR, "input[type='file']"):
                return True
        except Exception:
            pass
        try:
            # 一些实现用 label 覆盖 input
            if d.find_elements(By.XPATH, "//label[@for or @style or @class]"):
                return True
        except Exception:
            pass
        return clicked

    # ---------------- Author / Cover / Source -----------------
    def _fill_author(self, author_name: str) -> bool:
        d = self.ensure_driver()
        # 1) label → input
        xpaths = [
            "//label[contains(.,'作者')]/following::input[1]",
            "//*[contains(text(),'作者')]/following::input[1]",
            "//input[contains(@placeholder,'作者')]",
            "//input[@name='author' or @id='author']",
        ]
        el = None
        for xp in xpaths:
            try:
                el = d.find_element(By.XPATH, xp)
                break
            except Exception:
                continue
        if not el:
            # CSS fallbacks
            for css in ["input[placeholder*='作者']", "input[name='author']", "input#author"]:
                el = self._find(css)
                if el:
                    break
        if not el:
            # 2) JS 近邻搜索：找到含“作者”文本的容器，内部找可见的 input[type=text]
            js = r"""
                function visible(el){
                    if (!el) return false;
                    const style = window.getComputedStyle(el);
                    return style && style.display !== 'none' && style.visibility !== 'hidden' && el.offsetParent !== null;
                }
                const texts = Array.from(document.querySelectorAll('body *')).filter(n=>{
                    try{ return (n.innerText||'').includes('作者'); }catch(e){ return false; }
                });
                for (const node of texts){
                    // 往上找最近的容器
                    let cur = node;
                    for (let i=0; i<4; i++){
                        if (!cur) break;
                        const inputs = Array.from(cur.querySelectorAll('input[type="text"], input'));
                        for (const inp of inputs){
                            if (visible(inp)) return inp;
                        }
                        cur = cur.parentElement;
                    }
                }
                // 全局兜底：找一个最可能的空白文本输入框
                const allInputs = Array.from(document.querySelectorAll('input[type="text"], input'));
                for (const inp of allInputs){
                    if (visible(inp)) return inp;
                }
                return null;
            """
            try:
                el = d.execute_script(js)
            except Exception:
                el = None
        # 避免把标题输入误识别为作者输入
        if el:
            try:
                title_el = self._find_any(self.selectors.title_input_css, "#title", "input#title")
                if title_el and el == title_el:
                    el = None
            except Exception:
                pass
        if el:
            return self._safe_send_keys(el, author_name) or self._js_force_input(el, author_name)
        return False

    def _ensure_cover_section(self) -> bool:
        """滚动到封面区域，确保"单封面"选项可见（不需要展开，默认就可见）。"""
        d = self.ensure_driver()
        # 优先通过"单封面"选项本身定位并滚动
        try:
            single_cover_option = d.find_element(By.XPATH, "//*[contains(@class,'article-write-article-cover_normal-option')][.//span[normalize-space()='单封面']]")
            d.execute_script("arguments[0].scrollIntoView({block:'center'});", single_cover_option)
            time.sleep(0.3 if self.config.slow_mode else 0.2)
            return True
        except Exception:
            pass
        
        # 如果找不到"单封面"，降级滚动到封面区域
        for xp in [
            "//*[contains(@class,'article-write-article-cover')]",
            "//*[contains(text(),'封面')]/ancestor::*[self::section or self::div][1]",
            "//*[contains(text(),'封面')]",
            "//*[contains(@class,'cover')][.//button[contains(.,'从正文中选择')]]",
        ]:
            try:
                el = d.find_element(By.XPATH, xp)
                d.execute_script("arguments[0].scrollIntoView({block:'center'});", el)
                time.sleep(0.3 if self.config.slow_mode else 0.2)
                return True
            except Exception:
                continue
        return False

    def _find_cover_container(self):
        """查找封面设置区域的容器元素，尽量精确到包含选择按钮的块。找不到返回 None。"""
        d = self.ensure_driver()
        # 1) 优先根据“封面”文案的上层容器
        for xp in [
            "//*[contains(text(),'封面')]/ancestor::*[self::section or self::div][1]",
            "//*[contains(text(),'封面')]/ancestor::*[self::div[contains(@class,'section')]][1]",
        ]:
            try:
                return d.find_element(By.XPATH, xp)
            except Exception:
                continue
        # 2) 根据已知类名
        for css in [
            ".article-write-article-cover",
            "[class*='article-write-article-cover']",
        ]:
            try:
                el = d.find_element(By.CSS_SELECTOR, css)
                return el
            except Exception:
                continue
        # 3) 兜底：找到包含“从正文中选择”的按钮所在的最近容器
        for xp in [
            "//button[contains(.,'从正文中选择')]/ancestor::*[self::section or self::div][1]",
        ]:
            try:
                return d.find_element(By.XPATH, xp)
            except Exception:
                continue
        return None

    def _find_horizontal_cover_container(self):
        """定位横版封面容器（优先类名，其次基于文案的近邻容器）。"""
        d = self.ensure_driver()
        # 1) 仅返回横版“列表容器” normal-items，避免误拿到单个 cover 或竖版容器
        for css in [
            ".article-write-article-cover_normal-items",
            "[class*='article-write-article-cover_normal-items']",
        ]:
            try:
                return d.find_element(By.CSS_SELECTOR, css)
            except Exception:
                continue
        # 2) 基于“横版/横板封面”文案向上取最近容器
        for xp in [
            "//*[contains(text(),'横版封面') or contains(text(),'横板封面')]/ancestor::*[self::section or self::div][1]",
            "//*[contains(@class,'article-write-article-cover_normal-items')]",
        ]:
            try:
                return d.find_element(By.XPATH, xp)
            except Exception:
                continue
        return None

    def _find_vertical_cover_container(self):
        """定位竖版封面容器（按你提供的类名优先）。"""
        d = self.ensure_driver()
        for css in [
            ".article-write-article-cover_vertical-item-cover",
            "[class*='article-write-article-cover_vertical-item-cover']",
        ]:
            try:
                return d.find_element(By.CSS_SELECTOR, css)
            except Exception:
                continue
        # 兜底：基于“竖版封面”文案
        for xp in [
            "//*[contains(text(),'竖版封面')]/ancestor::*[self::section or self::div][1]",
        ]:
            try:
                return d.find_element(By.XPATH, xp)
            except Exception:
                continue
        return None

    def _open_pick_from_content_horizontal(self) -> bool:
        """按照最新交互流程打开横版封面“从正文中选择”弹窗。"""
        d = self.ensure_driver()
        container = self._find_horizontal_cover_container()
        if not container:
            return False

        # 1. 鼠标移动到横版封面容器，使“从正文中选择”按钮可见
        try:
            from selenium.webdriver.common.action_chains import ActionChains
            ActionChains(d).move_to_element(container).pause(0.3).perform()
            time.sleep(0.3)
        except Exception:
            pass

        # 2. 精准查找 data-spm-anchor-id 含 content.i0 的按钮
        button_selectors = [
            "div.article-write-article-cover-cover-item_choose button[data-spm-anchor-id*='content.i0']",
            "button.w-btn.w-btn_primary[data-spm-anchor-id*='content.i0']",
            "button[data-spm-anchor-id*='content.i0']",
        ]
        btn = None
        for css in button_selectors:
            try:
                btn = container.find_element(By.CSS_SELECTOR, css)
                break
            except Exception:
                btn = None
        if btn is None:
            try:
                btn = container.find_element(By.XPATH, ".//button[normalize-space()='从正文中选择' or contains(.,'从正文中选择')]")
            except Exception:
                btn = None
        if btn is None:
            return False

        # 3. 点击按钮，若失败则使用 JS 兜底
        try:
            d.execute_script("arguments[0].scrollIntoView({block:'center'});", btn)
        except Exception:
            pass
        clicked = False
        try:
            btn.click()
            clicked = True
        except Exception:
            try:
                d.execute_script("arguments[0].click();", btn)
                clicked = True
            except Exception:
                clicked = False

        if not clicked:
            return False

        try:
            key = "click_text:从正文中选择(横)"
            self._click_counts[key] = self._click_counts.get(key, 0) + 1
        except Exception:
            pass

        # 4. 等待弹窗出现
        return self._wait_cover_picker_modal(5.0)

    def _open_pick_from_content_vertical(self) -> bool:
        """在竖版封面容器内点击“从正文中选择”或“设置封面”入口。"""
        d = self.ensure_driver()
        container = self._find_vertical_cover_container()
        if container is not None:
            for css in [
                ".article-write-article-cover-cover-item_choose .w-btn.w-btn_primary",
                ".article-write-article-cover-cover-item_choose button",
            ]:
                try:
                    btn = container.find_element(By.CSS_SELECTOR, css)
                    d.execute_script("arguments[0].scrollIntoView({block:'center'});", btn)
                    try:
                        btn.click()
                    except Exception:
                        d.execute_script("arguments[0].click();", btn)
                    try:
                        key = "click_text:从正文中选择(竖)"
                        self._click_counts[key] = self._click_counts.get(key, 0) + 1
                    except Exception:
                        pass
                    time.sleep(0.2 if self.config.slow_mode else 0.1)
                    return True
                except Exception:
                    continue
            for xp in [
                ".//*[normalize-space()='设置封面' or contains(.,'设置封面')]",
                ".//*[contains(@class,'wm-icon-pic')]/ancestor::*[self::div or self::button][1]",
            ]:
                try:
                    el = container.find_element(By.XPATH, xp)
                    d.execute_script("arguments[0].scrollIntoView({block:'center'});", el)
                    try:
                        el.click()
                    except Exception:
                        d.execute_script("arguments[0].click();", el)
                    time.sleep(0.2 if self.config.slow_mode else 0.1)
                    return True
                except Exception:
                    continue
        return False

    def _click_text_button(self, texts: list[str]) -> bool:
        d = self.ensure_driver()
        # 0) 优先在弹窗/对话框内寻找，避免误点侧边栏
        def click_in_dialog(texts_local: list[str]) -> bool:
            dialog_roots = []
            try:
                dialog_roots = d.find_elements(By.XPATH, "//*[@role='dialog']|//*[contains(@class,'modal') or contains(@class,'dialog') or contains(@class,'ant-modal') or contains(@class,'dy-dialog') or contains(@class,'el-dialog')]")
            except Exception:
                dialog_roots = []
            for root in dialog_roots:
                for t in texts_local:
                    for xp in [
                        f".//button[normalize-space()='{t}' or contains(.,'{t}')]",
                        f".//*[@role='button' and (normalize-space()='{t}' or contains(.,'{t}'))]",
                        f".//a[@role='button' and (normalize-space()='{t}' or contains(.,'{t}'))]",
                    ]:
                        try:
                            el = root.find_element(By.XPATH, xp)
                            d.execute_script("arguments[0].scrollIntoView({block:'center'});", el)
                            try:
                                el.click()
                            except Exception:
                                d.execute_script("arguments[0].click();", el)
                            try:
                                key = f"click_text_dialog:{t}"
                                self._click_counts[key] = self._click_counts.get(key, 0) + 1
                            except Exception:
                                pass
                            time.sleep(0.2 if self.config.slow_mode else 0.1)
                            return True
                        except Exception:
                            continue
            return False

        if click_in_dialog(texts):
            return True

        # 1) 全局仅限 button
        for t in texts:
            try:
                btn = d.find_element(By.XPATH, f"//button[normalize-space()='{t}' or .//span[normalize-space()='{t}'] or contains(.,'{t}')]")
            except Exception:
                btn = None
            if not btn:
                continue
            # 过滤侧边栏/导航
            try:
                in_nav = d.execute_script('return !!arguments[0].closest("aside,nav,header,[class*=\'menu\'],[class*=\'nav\'],[class*=\'sider\'],[class*=\'sidebar\']");', btn)
                if in_nav:
                    continue
            except Exception:
                pass
            try:
                d.execute_script("arguments[0].scrollIntoView({block:'center'});", btn)
            except Exception:
                pass
            try:
                btn.click()
            except Exception:
                d.execute_script("arguments[0].click();", btn)
            try:
                key = f"click_text:{t}"
                self._click_counts[key] = self._click_counts.get(key, 0) + 1
            except Exception:
                pass
            time.sleep(0.2 if self.config.slow_mode else 0.1)
            return True

        # 2) 辅助：仅在非导航区域内点击 role=button 的 a/span/div
        for t in texts:
            patterns = [
                f"//*[@role='button' and (normalize-space()='{t}' or contains(.,'{t}'))]",
                f"//a[@role='button' and (normalize-space()='{t}' or contains(.,'{t}'))]",
            ]
            for xp in patterns:
                try:
                    el = d.find_element(By.XPATH, xp)
                    in_nav = False
                    try:
                        in_nav = d.execute_script('return !!arguments[0].closest("aside,nav,header,[class*=\'menu\'],[class*=\'nav\'],[class*=\'sider\'],[class*=\'sidebar\']");', el)
                    except Exception:
                        in_nav = False
                    if in_nav:
                        continue
                    d.execute_script("arguments[0].scrollIntoView({block:'center'});", el)
                    try:
                        el.click()
                    except Exception:
                        d.execute_script("arguments[0].click();", el)
                    try:
                        key = f"click_text:{t}"
                        self._click_counts[key] = self._click_counts.get(key, 0) + 1
                    except Exception:
                        pass
                    time.sleep(0.2 if self.config.slow_mode else 0.1)
                    return True
                except Exception:
                    continue
        return False

    def _select_single_cover(self) -> bool:
        """点击选择"单封面"选项，基于测试脚本的成功经验优化"""
        d = self.ensure_driver()
        
        # 方法1：直接查找包含"单封面"文字的元素并点击
        try:
            single_cover_element = d.find_element(By.XPATH, "//*[contains(text(), '单封面')]")
            # 点击包含"单封面"文字的元素
            d.execute_script("arguments[0].click();", single_cover_element)
            time.sleep(1)
            return True
        except Exception:
            pass

        # 方法2：查找.w-radio类的div元素
        try:
            radio_divs = d.find_elements(By.CSS_SELECTOR, "div.w-radio")
            for i, div in enumerate(radio_divs):
                try:
                    # 查看div的父元素是否包含"单封面"文字
                    parent = div.find_element(By.XPATH, "./..")
                    if "单封面" in parent.text:
                        d.execute_script("arguments[0].click();", div)
                        time.sleep(1)
                        return True
                except:
                    continue
        except Exception:
            pass

        # 方法3：使用XPath查找包含"单封面"的标签，然后找其关联的radio
        try:
            xpath_patterns = [
                "//*[contains(text(), '单封面')]/..//div[contains(@class, 'w-radio')]",
                "//*[contains(text(), '单封面')]/preceding-sibling::*[contains(@class, 'radio')]",
                "//*[contains(text(), '单封面')]/following-sibling::*[contains(@class, 'radio')]",
                "//*[contains(text(), '单封面')]/..//*[contains(@class, 'radio')]"
            ]
            
            for pattern in xpath_patterns:
                try:
                    elements = d.find_elements(By.XPATH, pattern)
                    if elements:
                        for elem in elements:
                            try:
                                d.execute_script("arguments[0].click();", elem)
                                time.sleep(1)
                                return True
                            except:
                                continue
                        break
                except:
                    continue
        except Exception:
            pass

        # 方法4：查找radio input元素（兜底方案）
        try:
            radio_inputs = d.find_elements(By.CSS_SELECTOR, "input[type='radio']")
            for i, radio in enumerate(radio_inputs):
                try:
                    # 查找父元素是否包含"单封面"
                    parent = radio.find_element(By.XPATH, "./..")
                    if "单封面" in parent.text:
                        if not radio.is_selected():
                            d.execute_script("arguments[0].click();", radio)
                            time.sleep(1)
                            return True
                        else:
                            return True  # 已经选中
                except:
                    continue
        except Exception:
            pass

        return False
        # 点击 “单封面”：兼容未选态（无 wm-icon-yes）与选态结构，并附带调试信息
        d = self.ensure_driver()
        debug: dict[str, Any] = {"attempts": []}

        def _is_selected_state(options_el=None):
            try:
                options = options_el or d.find_element(By.CSS_SELECTOR, ".article-write-article-cover_normal-options")
            except Exception:
                options = None
            
            # 尝试定位 w-radio 元素（优先 i0，兜底 i3）
            radio = None
            try:
                radio = d.find_element(By.CSS_SELECTOR, "div.article-write-article-cover_normal-option .w-radio[data-spm-anchor-id*='content.i0']")
            except Exception:
                try:
                    radio = d.find_element(By.CSS_SELECTOR, "div.article-write-article-cover_normal-option .w-radio[data-spm-anchor-id*='content.i3']")
                except Exception:
                    radio = None
            
            try:
                container = d.find_element(By.XPATH, "//*[contains(@class,'article-write-article-cover_normal-option')][.//span[contains(@class,'article-write-article-cover_normal-option-text') and normalize-space()='单封面']]")
            except Exception:
                container = None

            def _check_radio(obj):
                if obj is None:
                    return False
                try:
                    cls = obj.get_attribute('class') or ''
                except Exception:
                    cls = ''
                return 'w-radio_checked' in cls or 'wm-icon-yes' in cls

            if _check_radio(radio):
                return True
            if container is not None:
                try:
                    cls_c = container.get_attribute('class') or ''
                    if 'article-write-article-cover_normal-option_active' in cls_c:
                        return True
                except Exception:
                    pass
                try:
                    rr = container.find_elements(By.CSS_SELECTOR, '.w-radio')
                    for rr_el in rr:
                        if _check_radio(rr_el):
                            return True
                except Exception:
                    pass
            if options is not None:
                try:
                    selected_text = d.execute_script(
                        "const root=arguments[0]; if(!root) return null;"
                        "const sel = root.querySelector('.article-write-article-cover_normal-option .w-radio.w-radio_checked');"
                        "if(!sel){return null;} const opt = sel.closest('.article-write-article-cover_normal-option');"
                        "if(!opt) return null; const txt = opt.querySelector('.article-write-article-cover_normal-option-text');"
                        "return txt ? (txt.textContent||'').trim() : null;",
                        options,
                    )
                    if selected_text == '单封面':
                        return True
                except Exception:
                    pass
            return False

        # 若已选中直接返回
        if _is_selected_state():
            debug['already_selected'] = True
            self._last_single_cover_debug = debug
            return True

        # 目标容器与元素
        options_container = None
        try:
            options_container = d.find_element(By.CSS_SELECTOR, ".article-write-article-cover_normal-options")
        except Exception:
            options_container = None
        try:
            single_container = d.find_element(By.XPATH, "//*[contains(@class,'article-write-article-cover_normal-options')]//*[contains(@class,'article-write-article-cover_normal-option')][.//span[contains(@class,'article-write-article-cover_normal-option-text') and normalize-space()='单封面']]")
        except Exception:
            single_container = None
        try:
            # 优先尝试 i0（用户提供的实际 DOM）
            single_radio = d.find_element(By.CSS_SELECTOR, "div.article-write-article-cover_normal-option .w-radio[data-spm-anchor-id*='content.i0']")
        except Exception:
            try:
                # 兜底尝试 i3（旧版本）
                single_radio = d.find_element(By.CSS_SELECTOR, "div.article-write-article-cover_normal-option .w-radio[data-spm-anchor-id*='content.i3']")
            except Exception:
                single_radio = None
        if single_container is None and single_radio is not None:
            try:
                single_container = d.execute_script("return arguments[0].closest('.article-write-article-cover_normal-option');", single_radio)
            except Exception:
                single_container = None

        # 第一阶段：优先点击"单封面"的 w-radio 元素或容器
        # 根据实际 DOM 结构：
        # <div class="...normal-option" data-spm-anchor-id="...content.i4">
        #   <div class="w-radio" data-spm-anchor-id="...content.i0"></div>  ← 最新版本
        #   <span>单封面</span>
        # </div>
        primary_targets = []
        
        # 策略0：直接点击 w-radio 元素（最精确，用户提供的 DOM）
        if single_radio is not None:
            primary_targets.append(('w_radio_direct', single_radio))
        
        # 策略1：通过"单封面"文本定位其父容器（最可靠）
        if single_container is not None:
            primary_targets.append(('container_text', single_container))
        
        # 策略2：通过容器 data-spm=i4 定位，并验证内部有"单封面"文本
        try:
            # 先找所有 i4 容器
            i4_containers = d.find_elements(By.CSS_SELECTOR, "div.article-write-article-cover_normal-option[data-spm-anchor-id*='content.i4']")
            for c in i4_containers:
                try:
                    # 检查是否包含"单封面"文本
                    span_text = c.find_element(By.CSS_SELECTOR, ".article-write-article-cover_normal-option-text").text.strip()
                    if span_text == '单封面':
                        primary_targets.append(('container_spm_i4_verified', c))
                        break
                except Exception:
                    continue
        except Exception:
            pass
        
        # 策略3：直接通过 data-spm=i2 的 span（单封面文本）定位
        try:
            span_i2 = d.find_element(By.CSS_SELECTOR, "span.article-write-article-cover_normal-option-text[data-spm-anchor-id*='content.i2']")
            if span_i2 and span_i2.text.strip() == '单封面':
                primary_targets.append(('span_i2', span_i2))
        except Exception:
            pass
        
        for kind, el in primary_targets:
            if el is None:
                continue
            try:
                d.execute_script("arguments[0].scrollIntoView({block:'center'});", el)
            except Exception:
                pass
            clicked = self._dispatch_click_sequence(el)
            debug['attempts'].append({"type": "primary", "kind": kind, "clicked": clicked})
            if clicked:
                try:
                    self._click_counts['single_cover_primary'] = self._click_counts.get('single_cover_primary', 0) + 1
                except Exception:
                    pass
                time.sleep(0.3 if self.config.slow_mode else 0.15)
                if _is_selected_state(options_container):
                    debug['selected_via'] = f'primary_{kind}'
                    self._last_single_cover_debug = debug
                    return True
                # 如果点击成功但未选中，记录并继续下一个策略
                debug['attempts'][-1]['verified'] = False

        # 第二阶段：尝试容器/文本/隐藏 input 点击并验证
        if single_container is not None:
            try:
                d.execute_script("arguments[0].scrollIntoView({block:'center'});", single_container)
            except Exception:
                pass
            attempt_nodes: list[tuple[str, Any]] = [('container', single_container)]
            try:
                label_span = single_container.find_element(By.CSS_SELECTOR, '.article-write-article-cover_normal-option-text')
                attempt_nodes.append(('text', label_span))
            except Exception:
                label_span = None
            if single_radio is not None:
                attempt_nodes.append(('radio', single_radio))
            try:
                hidden_input = single_container.find_element(By.CSS_SELECTOR, "input[type='radio']")
                attempt_nodes.append(('input', hidden_input))
            except Exception:
                hidden_input = None

            for kind, node in attempt_nodes:
                clicked = self._dispatch_click_sequence(node)
                if not clicked and kind == 'input' and node is not None:
                    try:
                        d.execute_script("arguments[0].checked = true; arguments[0].dispatchEvent(new Event('input', {bubbles:true})); arguments[0].dispatchEvent(new Event('change', {bubbles:true}));", node)
                        clicked = True
                    except Exception:
                        clicked = False
                debug['attempts'].append({"type": kind, "clicked": clicked})
                if clicked:
                    t0 = time.time()
                    while time.time() - t0 < 1.0:
                        if _is_selected_state(options_container):
                            try:
                                key = f'single_cover_{kind}'
                                self._click_counts[key] = self._click_counts.get(key, 0) + 1
                            except Exception:
                                pass
                            debug['selected_via'] = kind
                            self._last_single_cover_debug = debug
                            time.sleep(0.1)
                            return True
                        time.sleep(0.1)

        # 第三阶段：文案兜底
        if self._click_text_button(["单封面", "单图封面", "单图"]):
            debug['fallback'] = 'text_button'
            if _is_selected_state():
                debug['selected_via'] = 'text_button'
                self._last_single_cover_debug = debug
                return True

        # 第四阶段：XPath/CSS 兜底
        for xp in [
            "//*[contains(text(),'单封面')]/following::*[contains(@class,'w-radio') or @role='radio'][1]",
            "//*[contains(text(),'单封面')]/preceding::*[contains(@class,'w-radio') or @role='radio'][1]",
            "//*[contains(text(),'单封面')]/ancestor::*[self::section or self::div][1]//*[contains(@class,'w-radio') or @role='radio'][1]",
            "//div[contains(@class,'w-radio') and contains(.,'单封面')]",
        ]:
            try:
                el = d.find_element(By.XPATH, xp)
            except Exception:
                el = None
            if not el:
                continue
            try:
                d.execute_script("arguments[0].scrollIntoView({block:'center'});", el)
            except Exception:
                pass
            clicked = self._dispatch_click_sequence(el)
            debug['attempts'].append({"type": "xpath", "selector": xp, "clicked": clicked})
            if clicked:
                time.sleep(0.2 if self.config.slow_mode else 0.1)
                if _is_selected_state():
                    debug['selected_via'] = xp
                    self._last_single_cover_debug = debug
                    return True

        for css in [".article-write-article-cover_normal-option .w-radio", "[role='radio']"]:
            el = self._find(css)
            if not el:
                continue
            try:
                d.execute_script("arguments[0].scrollIntoView({block:'center'});", el)
            except Exception:
                pass
            clicked = self._dispatch_click_sequence(el)
            debug['attempts'].append({"type": "css_any", "selector": css, "clicked": clicked})
            if clicked and _is_selected_state():
                debug['selected_via'] = css
                self._last_single_cover_debug = debug
                return True

        debug['selected_via'] = None
        self._last_single_cover_debug = debug
        return False

    def _open_pick_from_content(self) -> bool:
        # 优先匹配你提供的 DOM 结构，在封面容器内精确点击
        d = self.ensure_driver()
        container = self._find_cover_container()
        # 1) 直接根据具体类名点击
        try:
            btns = d.find_elements(By.CSS_SELECTOR, ".article-write-article-cover-cover-item_choose .w-btn.w-btn_primary, .article-write-article-cover-cover-item_choose button")
        except Exception:
            btns = []
        for btn in btns:
            try:
                d.execute_script("arguments[0].scrollIntoView({block:'center'});", btn)
            except Exception:
                pass
            try:
                btn.click()
            except Exception:
                try:
                    d.execute_script("arguments[0].click();", btn)
                except Exception:
                    continue
            # 记录点击统计，统一口径
            try:
                key = "click_text:从正文中选择"
                self._click_counts[key] = self._click_counts.get(key, 0) + 1
            except Exception:
                pass
            if self._wait_cover_picker_modal(4.0):
                return True
            return bool(self._get_visible_modal_roots())
        # 2) 在封面容器内部按文本定位
        if container:
            for xp in [
                ".//button[normalize-space()='从正文中选择' or contains(.,'从正文中选择')]",
                ".//a[normalize-space()='从正文中选择' or contains(.,'从正文中选择')]",
                ".//*[self::span or self::div][normalize-space()='从正文中选择' or contains(.,'从正文中选择')]",
            ]:
                try:
                    btn = container.find_element(By.XPATH, xp)
                    d.execute_script("arguments[0].scrollIntoView({block:'center'});", btn)
                    try:
                        btn.click()
                    except Exception:
                        d.execute_script("arguments[0].click();", btn)
                    try:
                        key = "click_text:从正文中选择"
                        self._click_counts[key] = self._click_counts.get(key, 0) + 1
                    except Exception:
                        pass
                    if self._wait_cover_picker_modal(4.0):
                        return True
                    return bool(self._get_visible_modal_roots())
                except Exception:
                    continue
        # 3) 兼容其他文案（全局兜底）
        ok = self._click_text_button([
            "从正文中选择", "从正文选择", "正文选择",
            "添加横板封面", "添加横版封面", "横板封面", "横版封面",
            "添加竖版封面", "竖版封面",
        ])
        if ok:
            if self._wait_cover_picker_modal(4.0):
                return True
            return bool(self._get_visible_modal_roots())
        return False

    def _open_pick_from_content_button_by_anchor(self, anchor_id: str) -> bool:
        """根据 data-spm-anchor-id 点击封面选择按钮（需要先hover显示按钮）"""
        d = self.ensure_driver()
        cover_container = None
        # 1) 先找到封面容器，通过hover触发按钮显示
        try:
            # 找到包含指定 anchor_id 的按钮，然后向上找最近的 normal-item/cover-item 容器
            btn_probe = d.find_element(By.CSS_SELECTOR, f'[data-spm-anchor-id*="{anchor_id}"]')
            try:
                # 仅接受横版 normal-item 的祖先，避免误落到竖版
                cover_container = d.execute_script("return arguments[0].closest('.article-write-article-cover_normal-item');", btn_probe)
            except Exception:
                cover_container = None
            if not cover_container:
                # 找不到对应的横版容器时，后续尝试兜底方案
                pass
            
            # 滚动到容器并hover触发按钮显示
            d.execute_script("arguments[0].scrollIntoView({block:'center'});", cover_container)
            
            # 使用ActionChains进行鼠标悬停
            from selenium.webdriver.common.action_chains import ActionChains
            actions = ActionChains(d)
            actions.move_to_element(cover_container).perform()
            
            # 等待按钮显示
            time.sleep(1.0)
            
        except Exception:
            pass
        
        # 2) 精确匹配 data-spm-anchor-id 点击按钮（仅在定位到横版 normal-item 容器时执行）
        if cover_container is not None:
            try:
                btn = cover_container.find_element(By.CSS_SELECTOR, f'button[data-spm-anchor-id*="{anchor_id}"]')
                d.execute_script("arguments[0].scrollIntoView({block:'center'});", btn)
                try:
                    btn.click()
                except Exception:
                    d.execute_script("arguments[0].click();", btn)
                # 记录点击统计
                try:
                    key = f"click_anchor_id:{anchor_id}"
                    self._click_counts[key] = self._click_counts.get(key, 0) + 1
                except Exception:
                    pass
                time.sleep(0.2 if self.config.slow_mode else 0.1)
                return True
            except Exception:
                pass
        
        # 3) 兜底：在封面容器内按文本查找（也需要hover）
        container = self._find_horizontal_cover_container()
        if container:
            # 先hover容器
            try:
                from selenium.webdriver.common.action_chains import ActionChains
                actions = ActionChains(d)
                actions.move_to_element(container).perform()
                time.sleep(0.5)
            except Exception:
                pass
            for xp in [
                ".//button[normalize-space()='从正文中选择' or contains(.,'从正文中选择')]",
                ".//a[normalize-space()='从正文中选择' or contains(.,'从正文中选择')]",
            ]:
                try:
                    btn = container.find_element(By.XPATH, xp)
                    d.execute_script("arguments[0].scrollIntoView({block:'center'});", btn)
                    try:
                        btn.click()
                    except Exception:
                        d.execute_script("arguments[0].click();", btn)
                    try:
                        key = f"click_fallback_anchor:{anchor_id}"
                        self._click_counts[key] = self._click_counts.get(key, 0) + 1
                    except Exception:
                        pass
                    time.sleep(0.2 if self.config.slow_mode else 0.1)
                    return True
                except Exception:
                    continue
        
        return False

    def _wait_cover_picker_modal(self, timeout_sec: float = 4.5) -> bool:
        """等待封面选择弹窗/浮层出现，返回是否检测到。"""
        d = self.ensure_driver()
        t0 = time.time()
        while time.time() - t0 < timeout_sec:
            try:
                # 典型对话框容器
                for css in [
                    "[role='dialog']", ".w-modal", ".w-dialog", ".ant-modal", ".modal", "[aria-modal='true']",
                    "[class*='modal']", "[class*='dialog']"
                ]:
                    els = d.find_elements(By.CSS_SELECTOR, css)
                    for el in els:
                        try:
                            vis = d.execute_script("const el=arguments[0];const st=getComputedStyle(el);return st && st.display!=='none' && st.visibility!=='hidden' && el.offsetParent!==null;", el)
                        except Exception:
                            vis = True
                        if vis:
                            return True
            except Exception:
                pass
            time.sleep(0.1)
        return False

    def _get_visible_modal_roots(self):
        """返回当前页面中可见的弹窗根元素列表（按 DOM 顺序）。"""
        d = self.ensure_driver()
        roots = []
        selectors = [
            "[role='dialog']",
            ".w-modal", ".w-dialog", ".ant-modal", ".modal", "[aria-modal='true']",
            "[class*='modal']", "[class*='dialog']",
        ]
        for css in selectors:
            try:
                for el in d.find_elements(By.CSS_SELECTOR, css):
                    try:
                        vis = d.execute_script("const el=arguments[0];const st=getComputedStyle(el);return st && st.display!=='none' && st.visibility!=='hidden' && el.offsetParent!==null;", el)
                    except Exception:
                        vis = True
                    if vis:
                        roots.append(el)
            except Exception:
                continue
        return roots

    def _get_top_modal_root(self):
        """获取最上层（z-index 最大或最后一个可见）弹窗根元素。"""
        d = self.ensure_driver()
        roots = self._get_visible_modal_roots()
        if not roots:
            return None
        # 选 z-index 最大的；若取不到，则用最后一个可见根
        try:
            def z(el):
                try:
                    return int(d.execute_script("return parseInt(getComputedStyle(arguments[0]).zIndex)||0;", el))
                except Exception:
                    return 0
            roots_sorted = sorted(roots, key=lambda e: z(e))
            return roots_sorted[-1] if roots_sorted else roots[-1]
        except Exception:
            return roots[-1]

    def _wait_no_modal(self, timeout_sec: float = 6.0) -> bool:
        """等待页面上所有弹窗关闭（无可见弹窗）。
        为了防止弹窗关闭后新弹窗立即打开导致的误判，
        在检测到无弹窗后，额外等待200ms以确保状态稳定。
        """
        t0 = time.time()
        no_modal_detected_at = None
        while time.time() - t0 < timeout_sec:
            try:
                if not self._get_visible_modal_roots():
                    if no_modal_detected_at is None:
                        no_modal_detected_at = time.time()
                    # 确保无弹窗状态持续至少200ms，避免短暂的"无弹窗间隙"
                    if time.time() - no_modal_detected_at >= 0.2:
                        return True
                else:
                    no_modal_detected_at = None
            except Exception:
                pass
            time.sleep(0.05)
        return not self._get_visible_modal_roots()

    def _try_close_top_modal(self) -> bool:
        """尝试点击顶层弹窗的关闭按钮（X/关闭），返回是否有动作执行。"""
        d = self.ensure_driver()
        root = self._get_top_modal_root()
        if not root:
            return True
        selectors = [
            ".ant-modal-close", ".w-modal .w-modal__close", ".w-dialog .w-dialog__close", ".modal .close", ".el-dialog__headerbtn",
            "[aria-label='Close']", "button[aria-label='Close']",
            ".ant-modal-close-x", ".ant-modal-close-icon",
        ]
        for css in selectors:
            try:
                btn = root.find_element(By.CSS_SELECTOR, css)
                d = self.ensure_driver()
                d.execute_script("arguments[0].scrollIntoView({block:'center'});", btn)
                try:
                    btn.click(); return True
                except Exception:
                    d.execute_script("arguments[0].click();", btn); return True
            except Exception:
                continue
        # 文本“关闭”兜底
        for xp in [
            ".//button[normalize-space()='关闭' or contains(.,'关闭')]",
            ".//*[@role='button' and (normalize-space()='关闭' or contains(.,'关闭'))]",
        ]:
            try:
                btn = root.find_element(By.XPATH, xp)
                d.execute_script("arguments[0].scrollIntoView({block:'center'});", btn)
                try:
                    btn.click(); return True
                except Exception:
                    d.execute_script("arguments[0].click();", btn); return True
            except Exception:
                continue
        return False

    def _open_pick_from_content_in_nth_horizontal(self, index: int) -> bool:
        """在第 index 个横版封面容器内点击“从正文中选择”。index 从 0 开始。"""
        d = self.ensure_driver()
        # 优先在“横版封面”整体容器内枚举子容器，避免把竖版容器算进去
        root = self._find_horizontal_cover_container()
        containers = []
        if root is not None:
            try:
                # 枚举每一个 normal-item，再在其内找唯一的 cover-item，保序且避免重复
                items = root.find_elements(By.XPATH, ".//div[contains(@class,'article-write-article-cover_normal-item')]")
            except Exception:
                items = []
            for it in items:
                try:
                    cover = it.find_element(By.CSS_SELECTOR, ".article-write-article-cover-cover-item")
                    containers.append(cover)
                except Exception:
                    continue
        # 严禁全局兜底，若未找到则返回 False，避免误打开竖版
        if not containers or index < 0 or index >= len(containers):
            return False
        target = containers[index]
        try:
            d.execute_script("arguments[0].scrollIntoView({block:'center'});", target)
        except Exception:
            pass
        # 悬停以显示按钮
        try:
            from selenium.webdriver.common.action_chains import ActionChains
            # 先 hover 整个 normal-item（target 的父），再 hover cover 本身
            try:
                parent_item = d.execute_script("return arguments[0].closest('.article-write-article-cover_normal-item')", target)
            except Exception:
                parent_item = None
            ac = ActionChains(d)
            if parent_item is not None:
                ac.move_to_element(parent_item).pause(0.2)
            ac.move_to_element(target).pause(0.2).perform()
            time.sleep(1.0)
        except Exception:
            pass
        # 优先 CSS 内部“从正文中选择”（尝试两次：hover 后与再次 hover choose 后）
        for attempt in range(2):
            try:
                btn = target.find_element(By.CSS_SELECTOR, 
                    ".article-write-article-cover-cover-item_choose .w-btn.w-btn_primary, .article-write-article-cover-cover-item_choose button")
            except Exception:
                btn = None
            if btn is not None:
                try:
                    visible = d.execute_script("const el=arguments[0];const st=getComputedStyle(el);return st && st.display!=='none' && st.visibility!=='hidden' && el.offsetParent!==null;", btn)
                except Exception:
                    visible = True
                if not visible:
                    try:
                        from selenium.webdriver.common.action_chains import ActionChains
                        choose = target.find_element(By.CSS_SELECTOR, ".article-write-article-cover-cover-item_choose")
                        ActionChains(d).move_to_element(choose).pause(0.2).perform()
                        time.sleep(0.4)
                    except Exception:
                        pass
                try:
                    btn.click()
                except Exception:
                    d.execute_script("arguments[0].click();", btn)
                try:
                    self._click_counts["open_horizontal_index"] = self._click_counts.get("open_horizontal_index", 0) + 1
                except Exception:
                    pass
                # 等待弹窗出现；若未出现，重试 hover+点击 一次
                if not self._wait_cover_picker_modal(4.0):
                    try:
                        from selenium.webdriver.common.action_chains import ActionChains
                        choose = target.find_element(By.CSS_SELECTOR, ".article-write-article-cover-cover-item_choose")
                        ActionChains(d).move_to_element(choose).pause(0.2).perform()
                        time.sleep(0.5)
                        try:
                            btn.click()
                        except Exception:
                            d.execute_script("arguments[0].click();", btn)
                    except Exception:
                        pass
                time.sleep(0.2 if self.config.slow_mode else 0.1)
                return True
        
        # 兜底文本匹配
        for xp in [
            ".//button[normalize-space()='从正文中选择' or contains(.,'从正文中选择')]",
            ".//a[normalize-space()='从正文中选择' or contains(.,'从正文中选择')]",
            ".//*[self::span or self::div][normalize-space()='从正文中选择' or contains(.,'从正文中选择')]",
            ".//*[normalize-space()='设置封面' or contains(.,'设置封面')]",
        ]:
            try:
                el = target.find_element(By.XPATH, xp)
                d.execute_script("arguments[0].scrollIntoView({block:'center'});", el)
                try:
                    from selenium.webdriver.common.action_chains import ActionChains
                    ActionChains(d).move_to_element(target).pause(0.2).perform()
                    time.sleep(0.3)
                except Exception:
                    pass
                try:
                    el.click()
                except Exception:
                    d.execute_script("arguments[0].click();", el)
                time.sleep(0.2 if self.config.slow_mode else 0.1)
                return True
            except Exception:
                continue
        return False

    def _select_multiple_covers_by_anchor(self, anchor_id: str, max_count: int) -> int:
        """处理多个相同data-spm-anchor-id的封面选择按钮，返回成功点击的数量"""
        d = self.ensure_driver()
        successful_clicks = 0
        
        try:
            # 找到所有匹配的按钮
            buttons = d.find_elements(By.CSS_SELECTOR, f'button[data-spm-anchor-id*="{anchor_id}"]')
            
            for i, btn in enumerate(buttons[:max_count]):  # 限制最大点击数量
                try:
                    d.execute_script("arguments[0].scrollIntoView({block:'center'});", btn)
                    try:
                        btn.click()
                    except Exception:
                        d.execute_script("arguments[0].click();", btn)
                    
                    # 记录点击统计
                    try:
                        key = f"click_multi_anchor_id:{anchor_id}_{i}"
                        self._click_counts[key] = self._click_counts.get(key, 0) + 1
                    except Exception:
                        pass
                    
                    successful_clicks += 1
                    time.sleep(0.2 if self.config.slow_mode else 0.1)
                    
                except Exception:
                    continue
                    
        except Exception:
            pass
            
        return successful_clicks

    def _pick_vertical_image_in_modal(self, prefer_vertical: bool) -> bool:
        d = self.ensure_driver()
        js = r"""
            const preferVertical = arguments[0];
            // 仅在真正的弹窗/浮层里选择图片，避免误点侧栏图标导致页面跳转
            function isVisible(el){
                if (!el) return false;
                const st = getComputedStyle(el);
                return st && st.display !== 'none' && st.visibility !== 'hidden' && el.offsetParent !== null;
            }
            function notInNav(el){
                return !el.closest('aside,nav,header,[class*="menu"],[class*="nav"],[class*="sider"],[class*="sidebar"]');
            }
            // 1) 常见弹窗容器选择器
            const modalSelectors = [
              '[role="dialog"]', '.modal', '.el-dialog', '.ant-modal', '.dy-dialog', '.w-modal', '.w-dialog',
              '[class*="modal"]', '[class*="dialog"]', '[aria-modal="true"]'
            ];
            let roots = [];
            for (const sel of modalSelectors){
                for (const el of document.querySelectorAll(sel)){
                    if (isVisible(el) && notInNav(el)) roots.push(el);
                }
            }
            // 2) 如果还没找到，尝试以“下一步/保存”按钮的近邻容器作为根
            if (!roots.length){
                const btnTexts = ['下一步','保存','确定','完成','使用'];
                const btns = Array.from(document.querySelectorAll('button, [role="button"]')).filter(b=>{
                    const t = (b.innerText||b.textContent||'').trim();
                    return btnTexts.some(x=>t.includes(x));
                });
                for (const b of btns){
                    const root = b.closest('section,div,article,main,form');
                    if (root && isVisible(root) && notInNav(root)) roots.push(root);
                }
            }
            // 3) 没有可用根则直接放弃（不要回退到 document 以免误点导航）
            if (!roots.length) return false;

            function score(img){
                const w = img.naturalWidth || img.width || 0;
                const h = img.naturalHeight || img.height || 0;
                if (!w || !h) return -1;
                const vertical = h > w;
                let s = 1;
                if (preferVertical && vertical) s += 5;
                if (!preferVertical && !vertical) s += 3;
                return s + Math.min((w*h)/10000, 5);
            }
            let best = null, bestScore = -1;
            for (const root of roots){
                // 限定在浮层中的缩略图
                const imgs = Array.from(root.querySelectorAll('img'))
                    .filter(img => isVisible(img) && notInNav(img));
                for (const img of imgs){
                    const s = score(img);
                    if (s > bestScore){ best = img; bestScore = s; }
                }
            }
            if (!best) return false;
            best.scrollIntoView({block:'center'});
            try{ best.click(); }catch(e){ try{ best.dispatchEvent(new MouseEvent('click', {bubbles:true})); }catch(e2){} }
            return true;
        """
        try:
            picked = d.execute_script(js, bool(prefer_vertical))
            # 如果未选中，尝试按环境变量指定的 src 精准选图
            if not picked:
                try:
                    target_src = os.getenv("DAYU_VERTICAL_COVER_IMG_SRC", "").strip()
                except Exception:
                    target_src = ""
                if target_src:
                    try:
                        # 仅在弹窗/浮层中查找该图片
                        img = None
                        for root_sel in ["[role='dialog']", ".modal", ".el-dialog", ".ant-modal", ".dy-dialog", ".w-modal", ".w-dialog"]:
                            try:
                                root = d.find_element(By.CSS_SELECTOR, root_sel)
                                try:
                                    img = root.find_element(By.CSS_SELECTOR, f"img[src='{target_src}']")
                                    break
                                except Exception:
                                    pass
                            except Exception:
                                continue
                        if img is None:
                            img = d.find_element(By.CSS_SELECTOR, f"img[src='{target_src}']")
                        d.execute_script("arguments[0].scrollIntoView({block:'center'});", img)
                        try:
                            img.click(); picked = True
                        except Exception:
                            d.execute_script("arguments[0].click();", img); picked = True
                    except Exception:
                        pass
            if picked:
                # 选图后优先点击带类名/数据属性的“下一步”
                step_clicked = False
                for css in [
                    "button.w-btn.w-btn_primary",
                    "button[data-spm-anchor-id*='i15']",
                ]:
                    try:
                        btn = d.find_element(By.CSS_SELECTOR, css)
                        if "下一步" in (btn.text or ""):
                            d.execute_script("arguments[0].scrollIntoView({block:'center'});", btn)
                            try:
                                btn.click(); step_clicked = True
                            except Exception:
                                d.execute_script("arguments[0].click();", btn); step_clicked = True
                            break
                    except Exception:
                        continue
                if not step_clicked:
                    self._click_text_button(["下一步"])  # 若不存在会忽略
                time.sleep(5)
                # 保存：优先点击带类名/数据属性的按钮
                saved = False
                for css in [
                    "button.w-btn.w-btn_primary",
                    "button[data-spm-anchor-id*='i6']",
                ]:
                    try:
                        btn = d.find_element(By.CSS_SELECTOR, css)
                        if "保存" in (btn.text or ""):
                            d.execute_script("arguments[0].scrollIntoView({block:'center'});", btn)
                            try:
                                btn.click(); saved = True
                            except Exception:
                                d.execute_script("arguments[0].click();", btn); saved = True
                            break
                    except Exception:
                        continue
                if not saved:
                    # 兼容“保存/确定/完成/使用”
                    if not self._click_text_button(["保存"]):
                        self._click_text_button(["确定", "完成", "使用该图片", "使用"])
            return bool(picked)
        except Exception:
            return False

    def _pick_first_image_in_modal(self) -> bool:
        """按照最新流程在弹窗中选择第一张图片并完成保存。"""
        d = self.ensure_driver()

        root = self._get_top_modal_root()
        if root is None:
            time.sleep(0.5)
            root = self._get_top_modal_root()
        if root is None:
            return False

        def _is_visible(el) -> bool:
            try:
                return bool(d.execute_script(
                    "const el=arguments[0];const st=getComputedStyle(el);return st && st.display!=='none' && st.visibility!=='hidden' && el.offsetParent!==null;",
                    el,
                ))
            except Exception:
                return True

        # 1. 选择第一张图片（优先 data-spm-anchor-id 包含 .i0）
        image_selectors = [
            "img[data-spm-anchor-id*='.i0']",
            "img[data-spm-anchor-id*='i0']",
            "img"
        ]
        target_img = None
        for css in image_selectors:
            try:
                candidates = root.find_elements(By.CSS_SELECTOR, css)
            except Exception:
                candidates = []
            for cand in candidates:
                if not _is_visible(cand):
                    continue
                target_img = cand
                break
            if target_img is not None:
                break
        if target_img is None:
            return False

        try:
            d.execute_script("arguments[0].scrollIntoView({block:'center'});", target_img)
        except Exception:
            pass
        clicked_img = False
        try:
            target_img.click()
            clicked_img = True
        except Exception:
            try:
                d.execute_script("arguments[0].click();", target_img)
                clicked_img = True
            except Exception:
                clicked_img = False
        if not clicked_img:
            return False

        time.sleep(1)

        # 2. 点击“下一步”按钮（优先 data-spm-anchor-id 含 .i1）
        next_selectors = [
            ("css", "button[data-spm-anchor-id*='.i1']"),
            ("css", "button[data-spm-anchor-id*='i1']"),
            ("xpath", ".//button[normalize-space()='下一步' or contains(.,'下一步')]")
        ]
        next_clicked = False
        for kind, selector in next_selectors:
            try:
                if kind == "css":
                    btn = root.find_element(By.CSS_SELECTOR, selector)
                else:
                    btn = root.find_element(By.XPATH, selector)
            except Exception:
                continue
            if not _is_visible(btn):
                continue
            try:
                d.execute_script("arguments[0].scrollIntoView({block:'center'});", btn)
            except Exception:
                pass
            try:
                btn.click()
                next_clicked = True
            except Exception:
                try:
                    d.execute_script("arguments[0].click();", btn)
                    next_clicked = True
                except Exception:
                    continue
            if next_clicked:
                break
        if not next_clicked:
            if not self._click_text_button(["下一步"]):
                return False

        # 3. 按要求等待 5 秒，确保弹窗进入保存步骤
        time.sleep(5)

        # 下一步后可能出现新的根弹窗
        root_after_next = self._get_top_modal_root() or root

        # 4. 点击“保存”按钮（优先 data-spm-anchor-id 含 .i7）
        save_selectors = [
            ("css", "button[data-spm-anchor-id*='.i7']"),
            ("css", "button[data-spm-anchor-id*='i7']"),
            ("xpath", ".//button[normalize-space()='保存' or contains(.,'保存')]")
        ]
        save_clicked = False
        for kind, selector in save_selectors:
            try:
                if kind == "css":
                    btn = root_after_next.find_element(By.CSS_SELECTOR, selector)
                else:
                    btn = root_after_next.find_element(By.XPATH, selector)
            except Exception:
                continue
            if not _is_visible(btn):
                continue
            try:
                d.execute_script("arguments[0].scrollIntoView({block:'center'});", btn)
            except Exception:
                pass
            try:
                btn.click()
                save_clicked = True
            except Exception:
                try:
                    d.execute_script("arguments[0].click();", btn)
                    save_clicked = True
                except Exception:
                    continue
            if save_clicked:
                break
        if not save_clicked:
            if not self._click_text_button(["保存", "使用"]):
                return False

        # 5. 等待弹窗关闭，失败则尝试关闭按钮
        if not self._wait_no_modal(6.0):
            self._try_close_top_modal()
            self._wait_no_modal(4.0)
        return True

    def _pick_image_in_modal_with_anchors(self, img_anchor: str | None, next_anchor: str | None, save_anchor: str | None) -> bool:
        """在当前顶层弹窗内按锚点顺序执行：
        1) 选中图片（优先 img[data-spm-anchor-id*=img_anchor]，否则第一张可见图片）
        2) 点击下一步（优先 button[data-spm-anchor-id*=next_anchor]，否则“下一步”文本）
        3) 点击保存（优先 button[data-spm-anchor-id*=save_anchor]，否则“保存”等文本）
        并在结束后等待弹窗关闭。
        """
        d = self.ensure_driver()
        try:
            root = self._get_top_modal_root()
            if root is None:
                time.sleep(0.8)
                root = self._get_top_modal_root()
            if root is None:
                return False
            # 选图
            picked = False
            if img_anchor:
                try:
                    img = root.find_element(By.CSS_SELECTOR, f"img[data-spm-anchor-id*='{img_anchor}']")
                    d.execute_script("arguments[0].scrollIntoView({block:'center'});", img)
                    try:
                        img.click(); picked = True
                    except Exception:
                        d.execute_script("arguments[0].click();", img); picked = True
                except Exception:
                    picked = False
            if not picked:
                try:
                    imgs = root.find_elements(By.CSS_SELECTOR, "img")
                except Exception:
                    imgs = []
                for im in imgs:
                    try:
                        vis = d.execute_script("const el=arguments[0];const st=getComputedStyle(el);return st && st.display!=='none' && st.visibility!=='hidden' && el.offsetParent!==null;", im)
                    except Exception:
                        vis = True
                    if not vis:
                        continue
                    try:
                        d.execute_script("arguments[0].scrollIntoView({block:'center'});", im)
                    except Exception:
                        pass
                    try:
                        im.click(); picked = True; break
                    except Exception:
                        try:
                            d.execute_script("arguments[0].click();", im); picked = True; break
                        except Exception:
                            continue
            if not picked:
                return False
            # 下一步
            step_clicked = False
            if next_anchor:
                try:
                    btn = root.find_element(By.CSS_SELECTOR, f"button[data-spm-anchor-id*='{next_anchor}']")
                    d.execute_script("arguments[0].scrollIntoView({block:'center'});", btn)
                    try:
                        btn.click(); step_clicked = True
                    except Exception:
                        d.execute_script("arguments[0].click();", btn); step_clicked = True
                except Exception:
                    step_clicked = False
            if not step_clicked:
                try:
                    btn = root.find_element(By.XPATH, ".//button[normalize-space()='下一步' or contains(.,'下一步')]")
                    d.execute_script("arguments[0].scrollIntoView({block:'center'});", btn)
                    try:
                        btn.click(); step_clicked = True
                    except Exception:
                        d.execute_script("arguments[0].click();", btn); step_clicked = True
                except Exception:
                    step_clicked = False
            if not step_clicked:
                self._click_text_button(["下一步"])  # 全局兜底
            time.sleep(8)  # 延长等待以确保页面完全转移到保存步骤
            # 保存
            saved = False
            new_root = self._get_top_modal_root() or root
            if save_anchor:
                try:
                    btn = new_root.find_element(By.CSS_SELECTOR, f"button[data-spm-anchor-id*='{save_anchor}']")
                    d.execute_script("arguments[0].scrollIntoView({block:'center'});", btn)
                    try:
                        btn.click(); saved = True
                    except Exception:
                        d.execute_script("arguments[0].click();", btn); saved = True
                except Exception:
                    saved = False
            if not saved:
                for xp in [
                    ".//button[normalize-space()='保存' or contains(.,'保存')]",
                ]:
                    try:
                        btn = new_root.find_element(By.XPATH, xp)
                        d.execute_script("arguments[0].scrollIntoView({block:'center'});", btn)
                        try:
                            btn.click(); saved = True
                        except Exception:
                            d.execute_script("arguments[0].click();", btn); saved = True
                        if saved:
                            break
                    except Exception:
                        continue
            if not saved:
                if not self._click_text_button(["保存"]):
                    self._click_text_button(["确定", "完成", "使用该图片", "使用"])  # 兼容其他文案

            # 等待弹窗关闭
            if not self._wait_no_modal(6.0):
                self._try_close_top_modal()
                self._wait_no_modal(4.0)
            return True
        except Exception:
            return False

    def _set_info_source(self, source_text: str) -> bool:
        d = self.ensure_driver()
        debug = {"container_found": False, "matched_method": None, "matched_text": None, "iframe_used": False}
        # 0) 若明确为“无需标注”，优先尝试直接点击该 radio（含你提供的 data-spm-anchor-id）
        try:
            if source_text.strip() == "无需标注":
                inp = d.find_element(By.XPATH, "//input[@type='radio' and @value='无需标注']")
                d.execute_script("arguments[0].scrollIntoView({block:'center'});", inp)
                try:
                    inp.click()
                except Exception:
                    d.execute_script("arguments[0].checked = true; arguments[0].dispatchEvent(new Event('input', {bubbles:true})); arguments[0].dispatchEvent(new Event('change', {bubbles:true}));", inp)
                time.sleep(0.15)
                debug["matched_method"] = "direct_radio_value=无需标注"
                debug["matched_text"] = "无需标注"
                self._last_info_source_debug = debug
                return True
        except Exception:
            pass
        # 1) 找到“信息来源”所在容器并滚动到视口（兼容 信息来源/内容来源/素材来源/来源）
        container = None
        for xp in [
            "//*[contains(text(),'信息来源') or contains(text(),'内容来源') or contains(text(),'素材来源') or contains(text(),'文章来源') or contains(text(),'来源标注') or contains(text(),'版权标注') or contains(text(),'来源信息') or contains(text(),'来源选择') or contains(text(),'来源类型') or normalize-space(text())='来源']/ancestor::*[self::section or self::div][1]",
            "//*[contains(text(),'信息来源') or contains(text(),'内容来源') or contains(text(),'素材来源') or contains(text(),'文章来源') or contains(text(),'来源标注') or contains(text(),'版权标注') or contains(text(),'来源信息') or contains(text(),'来源选择') or contains(text(),'来源类型') or normalize-space(text())='来源']",
        ]:
            try:
                el = d.find_element(By.XPATH, xp)
                try:
                    container = d.execute_script("return arguments[0].closest('section,div') || arguments[0];", el) or el
                except Exception:
                    container = el
                d.execute_script("arguments[0].scrollIntoView({block:'center'});", container)
                debug["container_found"] = True
                break
            except Exception:
                continue

        # 2) 目标文案集合（按类别区分同义词：自采/原创拍摄 vs 无需标注/网络素材）
        self_shot_syns = [
            "自行拍摄", "原创拍摄", "原创", "自采", "自制", "本人拍摄", "本人拍照", "自有素材",
            "原创图片", "原创内容", "现场拍摄", "自摄", "原创摄影", "拍摄",
        ]
        no_attr_syns = [
            "无需标注", "无须标注", "无需署名", "无需来源标注", "无来源标注", "不标注", "无需注明来源", "不注明来源",
            "网络素材", "网络来源", "素材来自网络", "素材来源网络", "转载", "非原创",
        ]
        norm_src = (source_text or "").strip()
        lowered = norm_src.lower()
        # 判定类别：包含明显关键词时，优先该类同义词
        choose_no_attr = any(k in norm_src for k in ["无需", "无须", "不标注", "不注明", "网络", "转载", "非原创"]) or any(s in norm_src for s in no_attr_syns)
        choose_self_shot = any(k in norm_src for k in ["原创", "拍摄", "自采", "自制"]) or any(s in norm_src for s in self_shot_syns)
        targets: list[str] = []
        if norm_src:
            targets.append(norm_src)
        if choose_no_attr:
            targets.extend([t for t in no_attr_syns if t not in targets])
        elif choose_self_shot:
            targets.extend([t for t in self_shot_syns if t not in targets])
        else:
            # 不确定时，保守地把两类都加入（以用户给定值优先）
            targets.extend([t for t in (self_shot_syns + no_attr_syns) if t not in targets])

        def _click_inside_container_by_text(texts: list[str]) -> bool:
            if not container:
                return False
            for t in texts:
                patterns = [
                    f".//button[normalize-space()='{t}' or contains(.,'{t}')]",
                    f".//a[normalize-space()='{t}' or contains(.,'{t}')]",
                    f".//*[self::span or self::div][normalize-space()='{t}' or contains(.,'{t}')]",
                ]
                for xp in patterns:
                    try:
                        btn = container.find_element(By.XPATH, xp)
                        d.execute_script("arguments[0].scrollIntoView({block:'center'});", btn)
                        try:
                            btn.click()
                        except Exception:
                            d.execute_script("arguments[0].click();", btn)
                        time.sleep(0.15)
                        debug["matched_method"] = "container_text"
                        debug["matched_text"] = t
                        return True
                    except Exception:
                        continue
                near_radio_xp = [
                    f".//*[contains(text(),'{t}')]/ancestor::*[self::label or @role='radio' or contains(@class,'w-radio')][1]",
                    f".//*[contains(.,'{t}') and (self::label or @role='radio' or contains(@class,'w-radio'))]",
                ]
                for xp in near_radio_xp:
                    try:
                        el2 = container.find_element(By.XPATH, xp)
                        d.execute_script("arguments[0].scrollIntoView({block:'center'});", el2)
                        try:
                            el2.click()
                        except Exception:
                            d.execute_script("arguments[0].click();", el2)
                        time.sleep(0.15)
                        debug["matched_method"] = "container_text_near_radio"
                        debug["matched_text"] = t
                        return True
                    except Exception:
                        continue
            return False

        # 3) 先在容器内点目标文案
        if _click_inside_container_by_text(targets):
            self._last_info_source_debug = debug
            return True

        # 3.1) 容器内直接选择 radio 输入（如 Antd: input.ant-radio-input[value='自行拍摄']）
        if container:
            for t in targets:
                radio_xps = [
                    f".//input[@type='radio' and @value='{t}']",
                    f".//input[@type='radio' and contains(@value,'{t}')]",
                ]
                for xp in radio_xps:
                    try:
                        inp = container.find_element(By.XPATH, xp)
                        d.execute_script("arguments[0].scrollIntoView({block:'center'});", inp)
                        clicked = False
                        try:
                            inp.click(); clicked = True
                        except Exception:
                            try:
                                wrap = d.execute_script(
                                    "return arguments[0].closest('label, .ant-radio-wrapper, .w-radio, [role=radio]');",
                                    inp,
                                )
                                if wrap:
                                    try:
                                        wrap.click(); clicked = True
                                    except Exception:
                                        d.execute_script("arguments[0].click();", wrap); clicked = True
                            except Exception:
                                pass
                        if not clicked:
                            try:
                                d.execute_script(
                                    "arguments[0].checked = true; arguments[0].dispatchEvent(new Event('input', {bubbles:true})); arguments[0].dispatchEvent(new Event('change', {bubbles:true}));",
                                    inp,
                                )
                                clicked = True
                            except Exception:
                                pass
                        if clicked:
                            time.sleep(0.2)
                            debug["matched_method"] = "container_radio_value"
                            debug["matched_text"] = t
                            self._last_info_source_debug = debug
                            return True
                    except Exception:
                        continue

        # 3.2) 容器内：基于包装元素文本匹配（如 .ant-radio-wrapper / .w-radio / label）
        if container:
            try:
                candidates = container.find_elements(
                    By.XPATH,
                    ".//*[self::label or @role='radio' or contains(@class,'radio') or contains(@class,'w-radio') or contains(@class,'ant-radio-wrapper')]",
                )
            except Exception:
                candidates = []
            for c in candidates:
                try:
                    txt = (c.text or "").strip()
                except Exception:
                    txt = ""
                if not txt:
                    continue
                if any(t in txt for t in targets):
                    try:
                        d.execute_script("arguments[0].scrollIntoView({block:'center'});", c)
                    except Exception:
                        pass
                    clicked = False
                    try:
                        c.click(); clicked = True
                    except Exception:
                        try:
                            d.execute_script("arguments[0].click();", c); clicked = True
                        except Exception:
                            clicked = False
                    if not clicked:
                        try:
                            inner = c.find_element(By.XPATH, ".//*[@role='radio' or self::input[@type='radio']]")
                            try:
                                inner.click(); clicked = True
                            except Exception:
                                d.execute_script("arguments[0].click();", inner); clicked = True
                        except Exception:
                            pass
                    if clicked:
                        time.sleep(0.2)
                        debug["matched_method"] = "container_radio_wrapper_text"
                        debug["matched_text"] = txt
                        self._last_info_source_debug = debug
                        return True

        # 4) 可能是下拉框：先点触发器，再在弹层选项中选择
        trigger_clicked = False
        if container:
            for xp in [
                ".//div[contains(@class,'select') or contains(@class,'dropdown') or contains(@class,'w-select')]",
                ".//*[@role='combobox' or @role='button']",
            ]:
                try:
                    trigger = container.find_element(By.XPATH, xp)
                    d.execute_script("arguments[0].scrollIntoView({block:'center'});", trigger)
                    try:
                        trigger.click()
                    except Exception:
                        d.execute_script("arguments[0].click();", trigger)
                    time.sleep(0.2)
                    trigger_clicked = True
                    break
                except Exception:
                    continue
        if trigger_clicked:
            pop_roots = [
                "//*[@role='dialog']",
                "//*[contains(@class,'modal') or contains(@class,'dialog') or contains(@class,'dropdown') or contains(@class,'popper') or contains(@class,'select-dropdown') or contains(@class,'el-select-dropdown') or contains(@class,'ant-select-dropdown')]",
            ]
            for root_xp in pop_roots:
                for t in targets:
                    try:
                        opt = d.find_element(By.XPATH, f"{root_xp}//*[normalize-space()='{t}' or contains(.,'{t}')]")
                        d.execute_script("arguments[0].scrollIntoView({block:'center'});", opt)
                        try:
                            opt.click()
                        except Exception:
                            d.execute_script("arguments[0].click();", opt)
                        time.sleep(0.15)
                        debug["matched_method"] = "dropdown_option"
                        debug["matched_text"] = t
                        self._last_info_source_debug = debug
                        return True
                    except Exception:
                        continue

        # 5) 全局兜底（A）：尝试直接勾选全局的 radio input
        for t in targets:
            for xp in [
                f"//input[@type='radio' and @value='{t}']",
                f"//input[@type='radio' and contains(@value,'{t}')]",
            ]:
                try:
                    inp = d.find_element(By.XPATH, xp)
                    d.execute_script("arguments[0].scrollIntoView({block:'center'});", inp)
                    clicked = False
                    try:
                        inp.click(); clicked = True
                    except Exception:
                        try:
                            wrap = d.execute_script(
                                "return arguments[0].closest('label, .ant-radio-wrapper, .w-radio, [role=radio]');",
                                inp,
                            )
                            if wrap:
                                try:
                                    wrap.click(); clicked = True
                                except Exception:
                                    d.execute_script("arguments[0].click();", wrap); clicked = True
                        except Exception:
                            pass
                    if not clicked:
                        try:
                            d.execute_script(
                                "arguments[0].checked = true; arguments[0].dispatchEvent(new Event('input', {bubbles:true})); arguments[0].dispatchEvent(new Event('change', {bubbles:true}));",
                                inp,
                            )
                            clicked = True
                        except Exception:
                            pass
                    if clicked:
                        time.sleep(0.15)
                        debug["matched_method"] = "global_radio_value"
                        debug["matched_text"] = t
                        self._last_info_source_debug = debug
                        return True
                except Exception:
                    continue

        # 5.5) 全局兜底（B）：基于包装元素文本匹配（不依赖容器/表单结构）
        for t in targets:
            try:
                wrappers = d.find_elements(
                    By.XPATH,
                    f"//*[self::label or @role='radio' or contains(@class,'ant-radio-wrapper') or contains(@class,'w-radio') or contains(@class,'radio')][contains(.,'{t}')]",
                )
            except Exception:
                wrappers = []
            for w in wrappers:
                try:
                    d.execute_script("arguments[0].scrollIntoView({block:'center'});", w)
                except Exception:
                    pass
                clicked = False
                try:
                    w.click(); clicked = True
                except Exception:
                    try:
                        d.execute_script("arguments[0].click();", w); clicked = True
                    except Exception:
                        clicked = False
                if not clicked:
                    try:
                        inp = w.find_element(By.XPATH, ".//input[@type='radio']")
                        try:
                            inp.click(); clicked = True
                        except Exception:
                            d.execute_script("arguments[0].checked = true; arguments[0].dispatchEvent(new Event('input', {bubbles:true})); arguments[0].dispatchEvent(new Event('change', {bubbles:true}));", inp)
                            clicked = True
                    except Exception:
                        pass
                if clicked:
                    time.sleep(0.15)
                    # 验证：input checked 或 wrapper 选中态
                    ok = False
                    try:
                        inp2 = w.find_element(By.XPATH, ".//input[@type='radio']")
                        ok = bool(inp2.is_selected())
                    except Exception:
                        ok = False
                    if not ok:
                        try:
                            ok = bool(d.execute_script("return arguments[0].getAttribute('aria-checked')==='true' || /checked|selected/.test(arguments[0].className||'');", w))
                        except Exception:
                            ok = False
                    if ok:
                        debug["matched_method"] = "global_radio_wrapper_text"
                        debug["matched_text"] = t
                        self._last_info_source_debug = debug
                        return True

        # 6) 全局兜底（B）：按文字点击
        if self._click_text_button(targets):
            debug["matched_method"] = "global_text"
            self._last_info_source_debug = debug
            return True

        # 7) 验证：是否已有目标 radio 被选中
        for t in targets:
            for xp in [
                f"//input[@type='radio' and @value='{t}']",
                f"//input[@type='radio' and contains(@value,'{t}')]",
            ]:
                try:
                    inp = d.find_element(By.XPATH, xp)
                    checked = False
                    try:
                        checked = bool(inp.is_selected())
                    except Exception:
                        checked = False
                    if not checked:
                        try:
                            checked = bool(d.execute_script("return arguments[0].checked === true;", inp))
                        except Exception:
                            checked = False
                    if checked:
                        self._last_info_source_debug = debug
                        return True
                except Exception:
                    continue

        # 8) 在 iframe 内进行同样的选择
        frames = d.find_elements(By.TAG_NAME, "iframe")
        for f in frames:
            try:
                d.switch_to.frame(f)
                debug["iframe_used"] = True
                # 8.1 radio 精准匹配
                for t in targets:
                    for xp in [
                        f"//input[@type='radio' and @value='{t}']",
                        f"//input[@type='radio' and contains(@value,'{t}')]",
                    ]:
                        try:
                            inp = d.find_element(By.XPATH, xp)
                            d.execute_script("arguments[0].scrollIntoView({block:'center'});", inp)
                            clicked = False
                            try:
                                inp.click(); clicked = True
                            except Exception:
                                try:
                                    wrap = d.execute_script(
                                        "return arguments[0].closest('label, .ant-radio-wrapper, .w-radio, [role=radio]');",
                                        inp,
                                    )
                                    if wrap:
                                        try:
                                            wrap.click(); clicked = True
                                        except Exception:
                                            d.execute_script("arguments[0].click();", wrap); clicked = True
                                except Exception:
                                    pass
                            if not clicked:
                                try:
                                    d.execute_script(
                                        "arguments[0].checked = true; arguments[0].dispatchEvent(new Event('input', {bubbles:true})); arguments[0].dispatchEvent(new Event('change', {bubbles:true}));",
                                        inp,
                                    )
                                    clicked = True
                                except Exception:
                                    pass
                            if clicked:
                                ok = False
                                try:
                                    ok = bool(inp.is_selected())
                                except Exception:
                                    ok = False
                                if not ok:
                                    try:
                                        ok = bool(d.execute_script("return arguments[0].checked === true;", inp))
                                    except Exception:
                                        ok = False
                                if ok:
                                    debug["matched_method"] = "iframe_radio_value"
                                    debug["matched_text"] = t
                                    self._last_info_source_debug = debug
                                    d.switch_to.default_content()
                                    return True
                        except Exception:
                            continue
                # 8.2 iframe 文案点击兜底
                for t in targets:
                    for xp in [
                        f"//button[normalize-space()='{t}' or contains(.,'{t}')]",
                        f"//a[normalize-space()='{t}' or contains(.,'{t}')]",
                        f"//*[self::span or self::div][normalize-space()='{t}' or contains(.,'{t}')]",
                    ]:
                        try:
                            btn = d.find_element(By.XPATH, xp)
                            d.execute_script("arguments[0].scrollIntoView({block:'center'});", btn)
                            try:
                                btn.click()
                            except Exception:
                                d.execute_script("arguments[0].click();", btn)
                            ok = False
                            try:
                                inp = d.find_element(By.XPATH, f"//input[@type='radio' and @value='{t}']")
                                ok = bool(inp.is_selected())
                            except Exception:
                                ok = False
                            if ok:
                                debug["matched_method"] = "iframe_text"
                                debug["matched_text"] = t
                                self._last_info_source_debug = debug
                                d.switch_to.default_content()
                                return True
                        except Exception:
                            continue
                # 8.3 iframe 包装元素文本兜底
                for t in targets:
                    try:
                        wrappers = d.find_elements(
                            By.XPATH,
                            f"//*[self::label or @role='radio' or contains(@class,'ant-radio-wrapper') or contains(@class,'w-radio') or contains(@class,'radio')][contains(.,'{t}')]",
                        )
                    except Exception:
                        wrappers = []
                    for w in wrappers:
                        clicked = False
                        try:
                            w.click(); clicked = True
                        except Exception:
                            try:
                                d.execute_script("arguments[0].click();", w); clicked = True
                            except Exception:
                                clicked = False
                        if not clicked:
                            try:
                                inp = w.find_element(By.XPATH, ".//input[@type='radio']")
                                try:
                                    inp.click(); clicked = True
                                except Exception:
                                    d.execute_script("arguments[0].checked = true; arguments[0].dispatchEvent(new Event('input', {bubbles:true})); arguments[0].dispatchEvent(new Event('change', {bubbles:true}));", inp)
                                    clicked = True
                            except Exception:
                                pass
                        if clicked:
                            ok = False
                            try:
                                inp2 = w.find_element(By.XPATH, ".//input[@type='radio']")
                                ok = bool(inp2.is_selected())
                            except Exception:
                                ok = False
                            if not ok:
                                try:
                                    ok = bool(d.execute_script("return arguments[0].getAttribute('aria-checked')==='true' || /checked|selected/.test(arguments[0].className||'');", w))
                                except Exception:
                                    ok = False
                            if ok:
                                debug["matched_method"] = "iframe_radio_wrapper_text"
                                debug["matched_text"] = t
                                self._last_info_source_debug = debug
                                d.switch_to.default_content()
                                return True
            except Exception:
                pass
            finally:
                try:
                    d.switch_to.default_content()
                except Exception:
                    pass

        # 保存容器 outerHTML 以便调试
        try:
            if container is not None:
                outer = container.get_attribute('outerHTML') or ''
                if len(outer) > 1200:
                    outer = outer[:1200] + '...'
                debug["container_outerHTML"] = outer
        except Exception:
            pass
        try:
            self._last_info_source_debug = debug
        except Exception:
            pass
        return False

    def _auto_detect_title(self):
        """Heuristically find a title input/textarea if configured selector failed."""
        if not self.config.auto_detect:
            return None
        d = self.ensure_driver()
        candidates_js = """
        const result = [];
        const els = Array.from(document.querySelectorAll('input,textarea,[contenteditable=true]'));
        for (const el of els){
            const ph = (el.getAttribute('placeholder')||'').trim();
            const arial = (el.getAttribute('aria-label')||'').trim();
            const cls = el.className || '';
            const idv = el.id || '';
            const tag = el.tagName.toLowerCase();
            const scoreParts = [];
            let score = 0;
            const txt = (ph+ ' ' + arial + ' ' + cls + ' ' + idv).toLowerCase();
            if (/标题|title/.test(txt)) { score += 5; scoreParts.push('kw'); }
            if (tag === 'input' || tag === 'textarea') { score += 2; scoreParts.push(tag); }
            if (el.getAttribute('maxlength') && parseInt(el.getAttribute('maxlength')) < 200) { score += 1; scoreParts.push('maxlength'); }
            if (score > 0) {
                const rect = el.getBoundingClientRect();
                if (rect.width > 100 && rect.height < 120) { score += 1; scoreParts.push('rect'); }
                result.push({score, scoreParts, tag, placeholder: ph, aria: arial, id: idv, className: cls});
            }
        }
        result.sort((a,b)=>b.score-a.score);
        return result.slice(0,5);
        """
        try:
            data = d.execute_script(candidates_js)
            self._last_auto_title_candidates = data  # store for debug
            # Try to locate top candidate element again in Python to send keys
            if data:
                # Build CSS attempts by id or class
                for cand in data:
                    if cand.get('id'):
                        try:
                            el = d.find_element(By.ID, cand['id'])
                            return el
                        except Exception:
                            pass
                    class_name = cand.get('className') or ''
                    first_class = class_name.split()[0] if class_name else ''
                    if first_class:
                        try:
                            el = d.find_element(By.CSS_SELECTOR, f'.{first_class}')
                            return el
                        except Exception:
                            pass
            return None
        except Exception:
            return None

    def _auto_detect_content(self):
        """Heuristically find a rich text editable area."""
        if not self.config.auto_detect:
            return None
        d = self.ensure_driver()
        js = """
        const out = [];
        const els = Array.from(document.querySelectorAll('[contenteditable=true],div,section,article')); 
        for (const el of els){
            const tag = el.tagName.toLowerCase();
            if (el.getAttribute('contenteditable') === 'true') {
                const rect = el.getBoundingClientRect();
                if (rect.width > 200 && rect.height > 80) {
                    out.push({tag, id: el.id, className: el.className, ce: true, w: rect.width, h: rect.height});
                }
            }
        }
        out.sort((a,b)=> (b.w*b.h) - (a.w*a.h));
        return out.slice(0,5);
        """
        try:
            data = d.execute_script(js)
            self._last_auto_content_candidates = data
            if data:
                for cand in data:
                    # prefer id
                    if cand.get('id'):
                        try:
                            el = d.find_element(By.ID, cand['id'])
                            return el
                        except Exception:
                            pass
                    class_name = cand.get('className') or ''
                    first_class = class_name.split()[0] if class_name else ''
                    if first_class:
                        try:
                            el = d.find_element(By.CSS_SELECTOR, f'.{first_class}')
                            return el
                        except Exception:
                            pass
            return None
        except Exception:
            return None

    # ---------------- Public API -----------------
    def publish_article(
        self,
        title: str,
        content: str,
        images: List[str] | None = None,
        author_name: Optional[str] = None,
        cover_single: Optional[bool] = None,
        cover_from_content: Optional[bool] = None,
        cover_vertical: Optional[bool] = None,
        info_source: Optional[str] = None,
    ):
        start = time.time()
        d = self.ensure_driver()
        debug_artifacts = {}
        try:
            # 1) 进入页面并点击“发布图文”流程
            self.open_publish_page()
            self.ensure_logged_in()
            # _enter_editor 中已覆盖“发布图文”，此处确保再尝试一次
            try:
                self._enter_editor()
            except Exception:
                pass
            # 2) 输入标题
            title_ok = self._fill_title(title)
            auto_title_used = False
            if not title_ok:
                auto_el = self._auto_detect_title()
                if auto_el:
                    title_ok = self._safe_send_keys(auto_el, title)
                    auto_title_used = title_ok
            # 3) 输入正文
            content_ok = self._fill_content(content)
            auto_content_used = False
            if not content_ok:
                auto_content_el = self._auto_detect_content()
                if auto_content_el:
                    # attempt JS focus then send keys
                    try:
                        d.execute_script("arguments[0].focus();", auto_content_el)
                    except Exception:
                        pass
                    content_ok = self._safe_send_keys(auto_content_el, content)
                    auto_content_used = content_ok
            # 4) 插入图片
            uploaded_count = self._upload_images(images)
            content_image_count = self._count_images_in_content()

            # Resolve overrides vs config
            author_to_use = author_name or self.config.default_author_name
            cover_single_use = self._coalesce_bool(cover_single, self.config.cover_single)
            cover_from_content_use = self._coalesce_bool(cover_from_content, self.config.cover_from_content)
            cover_vertical_use = self._coalesce_bool(cover_vertical, self.config.cover_vertical)
            info_source_to_use = info_source or self.config.info_source

            # 5) 输入作者名称
            author_ok = None
            if author_to_use:
                author_ok = self._fill_author(author_to_use)

            # 6) 点击单封面
            cover_section_ok = None
            single_cover_ok = None
            pick_from_content_ok = None
            pick_vertical_ok = None
            info_source_ok = None
            h1_ok = False  # 默认未完成横版封面选择，保证后续引用安全

            # 7) 选择封面设置
            if any([cover_single_use, cover_from_content_use, cover_vertical_use, bool(info_source_to_use)]):
                cover_section_ok = self._ensure_cover_section()
                
                # 首先点击单封面选项
                if cover_single_use:
                    single_cover_ok = self._select_single_cover()
                
                # 选择封面图片（单个横板）
                h1_ok = True  # 确保变量始终已定义，避免 NameError
                # if cover_from_content_use:
                #     # 只选择第一个横版封面，按你提供的新流程执行
                #     self._wait_no_modal(6.0)
                #     # 优先使用精确的横版打开逻辑
                #     opened_h1 = self._open_pick_from_content_horizontal()
                #     if not opened_h1:
                #         # 兜底：按索引定位第一个横版卡片
                #         opened_h1 = self._open_pick_from_content_in_nth_horizontal(0)

                #     if opened_h1:
                #         pick_from_content_ok = True
                #         # 弹窗内：选第一张图片 -> 下一步 -> 等 5 秒 -> 保存
                #         h1_ok = self._pick_first_image_in_modal()
                #         # 等待弹窗完全关闭
                #         self._wait_no_modal(8.0)

                # 如果启用竖版封面，则继续设置竖版
                if cover_vertical_use and h1_ok:
                        # 等待缓冲时间，确保状态稳定后再进行竖版
                        try:
                            time.sleep(3)
                        except Exception:
                            pass
                        
                        self._wait_no_modal(6.0)
                        opened_v = self._open_pick_from_content_vertical()
                        if opened_v:
                            _ok_v = self._pick_vertical_image_in_modal(True)
                            pick_from_content_ok = True if pick_from_content_ok is None else pick_from_content_ok
                            pick_vertical_ok = pick_vertical_ok or _ok_v
                            self._wait_no_modal(6.0)
                
                # 8) 选择信息来源
                if info_source_to_use:
                    info_source_ok = self._set_info_source(info_source_to_use)

            # 9) 发表前等待 3 秒（便于你核对封面与信息来源）
            try:
                time.sleep(3)
            except Exception:
                pass
            # 10) 点击发表
            submit_ok = self._click_submit()
            
            # 11) 发表后等待10秒，便于观察结果再关闭浏览器
            if submit_ok:
                try:
                    print("文章已发表，等待10秒后关闭浏览器...")
                    time.sleep(10)
                except Exception:
                    pass
            
            elapsed = round(time.time() - start, 2)
            result = {
                "status": "submitted",
                "title": title,
                "content_length": len(content),
                "images_requested": len(images) if images else 0,
                "images_uploaded": uploaded_count,
                "content_image_count": content_image_count,
                "title_ok": title_ok,
                "content_ok": content_ok,
                "submit_clicked": submit_ok,
                "elapsed_sec": elapsed,
                "logged_in": self._is_logged_in(),
                "auto_title_used": auto_title_used,
                "auto_content_used": auto_content_used,
                "iframe_count": getattr(self, "_iframe_count", 0),
                "content_in_iframe": getattr(self, "_content_iframe_index", None) is not None,
                "content_iframe_index": getattr(self, "_content_iframe_index", None),
                "content_used_shadow": getattr(self, "_content_used_shadow", False),
                # extras
                "author_ok": author_ok,
                "cover_section_ok": cover_section_ok,
                "single_cover_ok": single_cover_ok,
                "single_cover_debug": getattr(self, "_last_single_cover_debug", None),
                "pick_from_content_ok": pick_from_content_ok,
                "pick_vertical_ok": pick_vertical_ok,
                "horizontal_covers_ok": {
                    "h1": h1_ok,
                },
                "info_source_ok": info_source_ok,
                "used_author_name": author_to_use,
                "used_cover_single": cover_single_use,
                "used_cover_from_content": cover_from_content_use,
                "used_cover_vertical": cover_vertical_use,
                "used_info_source": info_source_to_use,
                "upload_debug": getattr(self, "_last_upload_debug", None),
                "info_source_debug": getattr(self, "_last_info_source_debug", None),
                "click_counts": getattr(self, "_click_counts", {}),
            }
            if getattr(self, '_last_auto_title_candidates', None):
                result['auto_title_candidates'] = self._last_auto_title_candidates
                if self.config.debug_dump:
                    # extract limited outerHTML for first few candidates
                    try:
                        snippets = []
                        for cand in self._last_auto_title_candidates[:3]:
                            idv = cand.get('id')
                            class_name = (cand.get('className') or '').split()[0]
                            el_ref = None
                            if idv:
                                try:
                                    el_ref = d.find_element(By.ID, idv)
                                except Exception:
                                    pass
                            if not el_ref and class_name:
                                try:
                                    el_ref = d.find_element(By.CSS_SELECTOR, f'.{class_name}')
                                except Exception:
                                    pass
                            if el_ref:
                                outer = el_ref.get_attribute('outerHTML') or ''
                                if len(outer) > 300:
                                    outer = outer[:300] + '...'
                                snippets.append(outer)
                        if snippets:
                            result['auto_title_outerHTML'] = snippets
                    except Exception:
                        pass
            if getattr(self, '_last_auto_content_candidates', None):
                result['auto_content_candidates'] = self._last_auto_content_candidates
                if self.config.debug_dump:
                    try:
                        snippets = []
                        for cand in self._last_auto_content_candidates[:3]:
                            idv = cand.get('id')
                            class_name = (cand.get('className') or '').split()[0]
                            el_ref = None
                            if idv:
                                try:
                                    el_ref = d.find_element(By.ID, idv)
                                except Exception:
                                    pass
                            if not el_ref and class_name:
                                try:
                                    el_ref = d.find_element(By.CSS_SELECTOR, f'.{class_name}')
                                except Exception:
                                    pass
                            if el_ref:
                                outer = el_ref.get_attribute('outerHTML') or ''
                                if len(outer) > 400:
                                    outer = outer[:400] + '...'
                                snippets.append(outer)
                        if snippets:
                            result['auto_content_outerHTML'] = snippets
                    except Exception:
                        pass
            if self.config.debug_dump and (not title_ok or not content_ok):
                try:
                    page_source_path = None
                    screenshot_path = None
                    dump_dir = self.config.screenshot_dir or os.getenv("TEMP") or "."
                    ts = int(time.time())
                    page_source_path = os.path.join(dump_dir, f"dayu_page_{ts}.html")
                    with open(page_source_path, "w", encoding="utf-8") as f:
                        f.write(d.page_source)
                    screenshot_path = os.path.join(dump_dir, f"dayu_screen_{ts}.png")
                    try:
                        d.save_screenshot(screenshot_path)
                    except Exception:
                        screenshot_path = None
                    debug_artifacts = {"page_source": page_source_path, "screenshot": screenshot_path}
                except Exception:
                    pass
            if debug_artifacts:
                result["debug"] = debug_artifacts
            return result
        except TimeoutException as e:  # type: ignore
            return {"status": "timeout", "error": str(e)}
        except WebDriverException as e:  # type: ignore
            return {"status": "webdriver_error", "error": str(e)}
        except Exception as e:  # pragma: no cover
            return {"status": "error", "error": str(e)}
        finally:
            # 运行时再次读取环境变量，作为 keep_browser_open 的兜底覆盖，防止实例初始化前未设置环境变量导致仍然关闭
            try:
                keep_open_env = _bool("DAYU_KEEP_BROWSER_OPEN", False)
            except Exception:
                keep_open_env = False
            keep_open_effective = bool(self.config.keep_browser_open or keep_open_env)
            if not keep_open_effective:
                time.sleep(10)
                self.close()

    def close(self):  # safe close
        try:
            if self.driver:
                self.driver.quit()
        except Exception:
            pass
        self.driver = None
